diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a8c110bf..3a0ca111 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,4 @@ # These are supported funding model platforms +github: Alexays custom: https://paypal.me/ARouillard diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 00000000..03e5d707 --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -0,0 +1,27 @@ +name: freebsd + +on: [ push, pull_request ] + +jobs: + clang: + # Run actions in a FreeBSD vm on the macos-10.15 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-10.15 + steps: + - uses: actions/checkout@v2 + - name: Test in FreeBSD VM + uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0 + with: + 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 + run: | + meson build -Dman-pages=enabled + ninja -C build + meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 00000000..d4efbf8c --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,27 @@ +name: linux + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + distro: + - alpine + - archlinux + - debian + - fedora + - opensuse + + runs-on: ubuntu-latest + container: + image: alexays/waybar:${{ matrix.distro }} + + steps: + - uses: actions/checkout@v2 + - name: configure + run: meson -Dman-pages=enabled build + - name: build + run: ninja -C build + - name: test + run: meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 54c148f0..00000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: cpp - -services: - - docker - -git: - submodules: false - -env: - - distro: debian - - distro: archlinux - - distro: fedora - - distro: alpine - - distro: opensuse - -before_install: - - docker pull alexays/waybar:${distro} - - find . -type f \( -name '*.cpp' -o -name '*.h' \) -print0 | xargs -r0 clang-format -i - -script: - - echo FROM alexays/waybar:${distro} > Dockerfile - - echo ADD . /root >> Dockerfile - - docker build -t waybar . - - docker run waybar /bin/sh -c "cd /root && meson build -Dman-pages=enabled && ninja -C build" - -jobs: - include: - - os: freebsd - compiler: clang - env: - before_install: - - sudo sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf - - sudo pkg install -y date gtk-layer-shell gtkmm30 jsoncpp libdbusmenu - libfmt libmpdclient libudev-devd meson pulseaudio scdoc spdlog - script: - - meson build -Dman-pages=enabled - - ninja -C build diff --git a/Dockerfiles/alpine b/Dockerfiles/alpine index 7b718375..c0e032ff 100644 --- a/Dockerfiles/alpine +++ b/Dockerfiles/alpine @@ -2,4 +2,4 @@ FROM alpine:latest -RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev scdoc +RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev sndio-dev scdoc libxkbcommon diff --git a/Dockerfiles/archlinux b/Dockerfiles/archlinux index d8ae16fd..40a1b2e3 100644 --- a/Dockerfiles/archlinux +++ b/Dockerfiles/archlinux @@ -1,6 +1,6 @@ # vim: ft=Dockerfile -FROM archlinux/base:latest +FROM archlinux:base-devel RUN pacman -Syu --noconfirm && \ - pacman -S git meson base-devel libinput wayland wayland-protocols pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient --noconfirm + pacman -S git meson base-devel libinput wayland wayland-protocols pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection --noconfirm libxkbcommon diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 077aca86..026d8fdb 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -3,5 +3,5 @@ FROM debian:sid RUN apt-get update && \ - apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection && \ + apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection libgirepository1.0-dev libxkbcommon-dev libxkbregistry-dev libxkbregistry0 && \ apt-get clean diff --git a/Dockerfiles/fedora b/Dockerfiles/fedora index d75083c8..a61dcd3e 100644 --- a/Dockerfiles/fedora +++ b/Dockerfiles/fedora @@ -1,7 +1,12 @@ # vim: ft=Dockerfile -FROM fedora:32 +FROM fedora:latest -RUN dnf install sway meson git libinput-devel wayland-devel wayland-protocols-devel pugixml-devel egl-wayland-devel mesa-libEGL-devel mesa-libGLES-devel mesa-libgbm-devel libxkbcommon-devel libudev-devel pixman-devel gtkmm30-devel jsoncpp-devel scdoc -y && \ - dnf group install "C Development Tools and Libraries" -y && \ +RUN dnf install -y @c-development git-core meson scdoc 'pkgconfig(date)' \ + 'pkgconfig(dbusmenu-gtk3-0.4)' 'pkgconfig(fmt)' 'pkgconfig(gdk-pixbuf-2.0)' \ + 'pkgconfig(gio-unix-2.0)' 'pkgconfig(gtk-layer-shell-0)' 'pkgconfig(gtkmm-3.0)' \ + 'pkgconfig(jsoncpp)' 'pkgconfig(libinput)' 'pkgconfig(libmpdclient)' \ + 'pkgconfig(libnl-3.0)' 'pkgconfig(libnl-genl-3.0)' 'pkgconfig(libpulse)' \ + 'pkgconfig(libudev)' 'pkgconfig(pugixml)' 'pkgconfig(sigc++-2.0)' 'pkgconfig(spdlog)' \ + 'pkgconfig(wayland-client)' 'pkgconfig(wayland-cursor)' 'pkgconfig(wayland-protocols)' 'pkgconfig(xkbregistry)' && \ dnf clean all -y diff --git a/Dockerfiles/opensuse b/Dockerfiles/opensuse index 5b664fb2..49dea272 100644 --- a/Dockerfiles/opensuse +++ b/Dockerfiles/opensuse @@ -3,5 +3,7 @@ FROM opensuse/tumbleweed:latest 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 scdoc + zypper -n install git meson clang libinput10 libinput-devel pugixml-devel libwayland-client0 libwayland-cursor0 wayland-protocols-devel wayland-devel Mesa-libEGL-devel Mesa-libGLESv2-devel libgbm-devel libxkbcommon-devel libudev-devel libpixman-1-0-devel gtkmm3-devel jsoncpp-devel libxkbregistry-devel scdoc diff --git a/Makefile b/Makefile index d7182c18..94f8ee6e 100644 --- a/Makefile +++ b/Makefile @@ -16,5 +16,8 @@ install: build run: build ./build/waybar +debug-run: build-debug + ./build/waybar --log-level debug + clean: rm -rf build diff --git a/README.md b/README.md index f7bc4c89..98b99a2d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Waybar [![Travis](https://travis-ci.org/Alexays/Waybar.svg?branch=master)](https://travis-ci.org/Alexays/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) +# Waybar [![Licence](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Paypal Donate](https://img.shields.io/badge/Donate-Paypal-2244dd.svg)](https://paypal.me/ARouillard)
![Waybar](https://raw.githubusercontent.com/alexays/waybar/master/preview-2.png) > Highly customizable Wayland bar for Sway and Wlroots based compositors.
> Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or [AUR](https://aur.archlinux.org/packages/waybar-git/), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* -**Current features** +#### Current features - Sway (Workspaces, Binding mode, Focused window name) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time @@ -22,11 +22,21 @@ - Multiple output configuration - And much more customizations -**Configuration and Styling** +#### Configuration and Styling [See the wiki for more details](https://github.com/Alexays/Waybar/wiki). -**How to build** +### Installation + +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) + +An Ubuntu PPA with more recent versions is available +[here](https://launchpad.net/~nschloe/+archive/ubuntu/waybar). + + +#### Building from source ```bash $ git clone https://github.com/Alexays/Waybar @@ -57,6 +67,8 @@ libnl [Network module] libappindicator-gtk3 [Tray module] libdbusmenu-gtk3 [Tray module] libmpdclient [MPD module] +libsndio [sndio module] +libevdev [KeyboardState module] ``` **Build dependencies** @@ -75,6 +87,7 @@ sudo apt install \ clang-tidy \ gobject-introspection \ libdbusmenu-gtk3-dev \ + libevdev-dev \ libfmt-dev \ libgirepository1.0-dev \ libgtk-3-dev \ diff --git a/include/ALabel.hpp b/include/ALabel.hpp index d4ad94d3..d8a5b504 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -10,16 +10,15 @@ namespace waybar { class ALabel : public AModule { public: ALabel(const Json::Value &, const std::string &, const std::string &, const std::string &format, - uint16_t interval = 0, bool ellipsize = false); + uint16_t interval = 0, bool ellipsize = false, bool enable_click = false, bool enable_scroll = false); virtual ~ALabel() = default; virtual auto update() -> void; virtual std::string getIcon(uint16_t, const std::string &alt = "", uint16_t max = 0); - virtual std::string getIcon(uint16_t, std::vector &alts, uint16_t max = 0); + virtual std::string getIcon(uint16_t, const std::vector &alts, uint16_t max = 0); protected: Gtk::Label label_; std::string format_; - std::string click_param; const std::chrono::seconds interval_; bool alt_ = false; std::string default_format_; diff --git a/include/AModule.hpp b/include/AModule.hpp index f7cc484e..c9f1ae21 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -4,14 +4,15 @@ #include #include #include + #include "IModule.hpp" namespace waybar { class AModule : public IModule { public: - AModule(const Json::Value &, const std::string &, const std::string &, - bool enable_click = false, bool enable_scroll = false); + AModule(const Json::Value &, const std::string &, const std::string &, bool enable_click = false, + bool enable_scroll = false); virtual ~AModule(); virtual auto update() -> void; virtual operator Gtk::Widget &(); @@ -24,6 +25,7 @@ class AModule : public IModule { SCROLL_DIR getScrollDir(GdkEventScroll *e); bool tooltipEnabled(); + const std::string name_; const Json::Value &config_; Gtk::EventBox event_box_; diff --git a/include/bar.hpp b/include/bar.hpp index fb0cd592..6f3dfcf9 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -7,9 +7,8 @@ #include #include #include + #include "AModule.hpp" -#include "idle-inhibit-unstable-v1-client-protocol.h" -#include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar { @@ -18,65 +17,68 @@ class Factory; struct waybar_output { Glib::RefPtr monitor; std::string name; + std::string identifier; std::unique_ptr xdg_output = { nullptr, &zxdg_output_v1_destroy}; }; +enum class bar_layer : uint8_t { + BOTTOM, + TOP, + OVERLAY, +}; + +struct bar_margins { + int top = 0; + int right = 0; + int bottom = 0; + int left = 0; +}; + +class BarSurface { + protected: + BarSurface() = default; + + public: + virtual void setExclusiveZone(bool enable) = 0; + virtual void setLayer(bar_layer layer) = 0; + virtual void setMargins(const struct bar_margins &margins) = 0; + virtual void setPassThrough(bool enable) = 0; + virtual void setPosition(const std::string_view &position) = 0; + virtual void setSize(uint32_t width, uint32_t height) = 0; + virtual void commit(){}; + + virtual ~BarSurface() = default; +}; + class Bar { public: Bar(struct waybar_output *w_output, const Json::Value &); Bar(const Bar &) = delete; ~Bar() = default; - auto toggle() -> void; + void setVisible(bool visible); + void toggle(); void handleSignal(int); struct waybar_output *output; Json::Value config; - Gtk::Window window; struct wl_surface * surface; + bool exclusive = true; bool visible = true; bool vertical = false; + Gtk::Window window; private: - static constexpr const char *MIN_HEIGHT_MSG = - "Requested height: {} exceeds the minimum height: {} required by the modules"; - static constexpr const char *MIN_WIDTH_MSG = - "Requested width: {} exceeds the minimum width: {} required by the modules"; - 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"; - static void layerSurfaceHandleConfigure(void *, struct zwlr_layer_surface_v1 *, uint32_t, - uint32_t, uint32_t); - static void layerSurfaceHandleClosed(void *, struct zwlr_layer_surface_v1 *); - -#ifdef HAVE_GTK_LAYER_SHELL - void initGtkLayerShell(); -#endif - void onConfigure(GdkEventConfigure *ev); - void onRealize(); - void onMap(GdkEventAny *ev); - void setExclusiveZone(uint32_t width, uint32_t height); - void setSurfaceSize(uint32_t width, uint32_t height); + void onMap(GdkEventAny *); auto setupWidgets() -> void; void getModules(const Factory &, const std::string &); void setupAltFormatKeyForModule(const std::string &module_name); void setupAltFormatKeyForModuleList(const char *module_list_name); - struct margins { - int top = 0; - int right = 0; - int bottom = 0; - int left = 0; - } margins_; - struct zwlr_layer_surface_v1 *layer_surface_; - // use gtk-layer-shell instead of handling layer surfaces directly - bool use_gls_ = false; - uint32_t width_ = 0; - uint32_t height_ = 1; - uint8_t anchor_; + std::unique_ptr surface_impl_; + bar_layer layer_; Gtk::Box left_; Gtk::Box center_; Gtk::Box right_; diff --git a/include/client.hpp b/include/client.hpp index 39b6ae3b..bd80d0bd 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -3,10 +3,14 @@ #include #include #include -#include #include -#include + #include "bar.hpp" +#include "config.hpp" + +struct zwlr_layer_shell_v1; +struct zwp_idle_inhibitor_v1; +struct zwp_idle_inhibit_manager_v1; namespace waybar { @@ -14,6 +18,7 @@ class Client { public: static Client *inst(); int main(int argc, char *argv[]); + void reset(); Glib::RefPtr gtk_app; Glib::RefPtr gdk_display; @@ -23,28 +28,27 @@ class Client { struct zxdg_output_manager_v1 * xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; std::vector> bars; + Config config; private: Client() = default; - std::tuple getConfigs(const std::string &config, - const std::string &style) const; - void bindInterfaces(); - const std::string getValidPath(const std::vector &paths) const; - void handleOutput(struct waybar_output &output); - bool isValidOutput(const Json::Value &config, struct waybar_output &output); - auto setupConfig(const std::string &config_file) -> void; - auto setupCss(const std::string &css_file) -> void; - struct waybar_output &getOutput(void *); + const std::string getStyle(const std::string &style); + void bindInterfaces(); + void handleOutput(struct waybar_output &output); + auto setupCss(const std::string &css_file) -> void; + struct waybar_output & getOutput(void *); std::vector getOutputConfigs(struct waybar_output &output); static void handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version); static void handleGlobalRemove(void *data, struct wl_registry *registry, uint32_t name); + static void handleOutputDone(void *, struct zxdg_output_v1 *); static void handleOutputName(void *, struct zxdg_output_v1 *, const char *); + static void handleOutputDescription(void *, struct zxdg_output_v1 *, const char *); void handleMonitorAdded(Glib::RefPtr monitor); void handleMonitorRemoved(Glib::RefPtr monitor); + void handleDeferredMonitorRemoval(Glib::RefPtr monitor); - Json::Value config_; Glib::RefPtr style_context_; Glib::RefPtr css_provider_; std::list outputs_; diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 00000000..82d55995 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include + +#ifndef SYSCONFDIR +#define SYSCONFDIR "/etc" +#endif + +namespace waybar { + +class Config { + public: + static const std::vector CONFIG_DIRS; + + /* Try to find any of provided names in the supported set of config directories */ + static std::optional findConfigPath( + const std::vector &names, const std::vector &dirs = CONFIG_DIRS); + + Config() = default; + + void load(const std::string &config); + + Json::Value &getConfig() { return config_; } + + std::vector getOutputConfigs(const std::string &name, const std::string &identifier); + + private: + void setupConfig(Json::Value &dst, const std::string &config_file, int depth); + void resolveConfigIncludes(Json::Value &config, int depth); + void mergeConfig(Json::Value &a_config_, Json::Value &b_config_); + + std::string config_file_; + + Json::Value config_; +}; +} // namespace waybar diff --git a/include/factory.hpp b/include/factory.hpp index c698aa32..43dd2cfd 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -1,11 +1,23 @@ #pragma once #include +#ifdef HAVE_LIBDATE #include "modules/clock.hpp" +#else +#include "modules/simpleclock.hpp" +#endif #ifdef HAVE_SWAY #include "modules/sway/mode.hpp" #include "modules/sway/window.hpp" #include "modules/sway/workspaces.hpp" +#include "modules/sway/language.hpp" +#endif +#ifdef HAVE_WLR +#include "modules/wlr/taskbar.hpp" +#include "modules/wlr/workspace_manager.hpp" +#endif +#ifdef HAVE_RIVER +#include "modules/river/tags.hpp" #endif #if defined(__linux__) && !defined(NO_FILESYSTEM) #include "modules/battery.hpp" @@ -27,17 +39,25 @@ #ifdef HAVE_LIBUDEV #include "modules/backlight.hpp" #endif +#ifdef HAVE_LIBEVDEV +#include "modules/keyboard_state.hpp" +#endif #ifdef HAVE_LIBPULSE #include "modules/pulseaudio.hpp" #endif #ifdef HAVE_LIBMPDCLIENT -#include "modules/mpd.hpp" +#include "modules/mpd/mpd.hpp" +#endif +#ifdef HAVE_LIBSNDIO +#include "modules/sndio.hpp" #endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" #if defined(__linux__) -#include "modules/bluetooth.hpp" +# ifdef WANT_RFKILL +# include "modules/bluetooth.hpp" +# endif #endif namespace waybar { diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index d4d20d1e..41bc0ad3 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -31,19 +31,22 @@ class Battery : public ALabel { private: static inline const fs::path data_dir_ = "/sys/class/power_supply/"; - void getBatteries(); - void worker(); - const std::string getAdapterStatus(uint8_t capacity) const; - const std::tuple getInfos() const; - const std::string formatTimeRemaining(float hoursRemaining); + void refreshBatteries(); + void worker(); + const std::string getAdapterStatus(uint8_t capacity) const; + const std::tuple getInfos(); + const std::string formatTimeRemaining(float hoursRemaining); - std::vector batteries_; + int global_watch; + std::map batteries_; fs::path adapter_; - int fd_; - std::vector wds_; + int battery_watch_fd_; + int global_watch_fd_; + std::mutex battery_list_mutex_; std::string old_status_; util::SleeperThread thread_; + util::SleeperThread thread_battery_update_; util::SleeperThread thread_timer_; }; diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index 04c213da..87845c95 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -1,10 +1,6 @@ #pragma once -#include #include "ALabel.hpp" - -#include -#include "util/sleeper_thread.hpp" #include "util/rfkill.hpp" namespace waybar::modules { @@ -16,10 +12,6 @@ class Bluetooth : public ALabel { auto update() -> void; private: - std::string status_; - util::SleeperThread thread_; - util::SleeperThread intervall_thread_; - util::Rfkill rfkill_; }; diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index e3873a6d..17752e4d 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -28,12 +28,16 @@ class Clock : public ALabel { std::locale locale_; const date::time_zone* time_zone_; bool fixed_time_zone_; - date::year_month_day cached_calendar_ymd_; + int time_zone_idx_; + date::year_month_day cached_calendar_ymd_ = date::January/1/0; std::string cached_calendar_text_; + bool handleScroll(GdkEventScroll* e); + auto calendar_text(const waybar_time& wtime) -> std::string; auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void; auto first_day_of_week() -> date::weekday; + bool setTimeZone(Json::Value zone_name); }; } // namespace waybar::modules diff --git a/include/modules/cpu.hpp b/include/modules/cpu.hpp index 7a703364..7e32a43f 100644 --- a/include/modules/cpu.hpp +++ b/include/modules/cpu.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -20,9 +19,11 @@ class Cpu : public ALabel { auto update() -> void; private: - uint16_t getCpuLoad(); - std::tuple getCpuUsage(); - std::vector> parseCpuinfo(); + double getCpuLoad(); + std::tuple, std::string> getCpuUsage(); + std::tuple getCpuFrequency(); + std::vector> parseCpuinfo(); + std::vector parseCpuFrequencies(); std::vector> prev_times_; diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index b8dad9dd..7c771450 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -22,6 +22,7 @@ class Custom : public ALabel { void continuousWorker(); void parseOutputRaw(); void parseOutputJson(); + void handleEvent(); bool handleScroll(GdkEventScroll* e); bool handleToggle(GdkEventButton* const& e); diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 5ce324da..4b6c097f 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -12,12 +12,13 @@ class IdleInhibitor : public ALabel { IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); ~IdleInhibitor(); auto update() -> void; + static std::list modules; + static bool status; private: bool handleToggle(GdkEventButton* const& e); const Bar& bar_; - std::string status_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; }; diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp new file mode 100644 index 00000000..1793bfe8 --- /dev/null +++ b/include/modules/keyboard_state.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#if FMT_VERSION < 60000 +#include +#else +#include +#endif +#include "AModule.hpp" +#include "bar.hpp" +#include "util/sleeper_thread.hpp" +#include + +extern "C" { +#include +} + +namespace waybar::modules { + +class KeyboardState : public AModule { + public: + KeyboardState(const std::string&, const waybar::Bar&, const Json::Value&); + ~KeyboardState(); + auto update() -> void; + + private: + static auto openDevice(const std::string&) -> std::pair; + + Gtk::Box box_; + Gtk::Label numlock_label_; + Gtk::Label capslock_label_; + Gtk::Label scrolllock_label_; + + std::string numlock_format_; + std::string capslock_format_; + std::string scrolllock_format_; + const std::chrono::seconds interval_; + std::string icon_locked_; + std::string icon_unlocked_; + + int fd_; + libevdev* dev_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/mpd.hpp b/include/modules/mpd.hpp deleted file mode 100644 index d08b28b2..00000000 --- a/include/modules/mpd.hpp +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "ALabel.hpp" - -namespace waybar::modules { - -class MPD : public ALabel { - public: - MPD(const std::string&, const Json::Value&); - auto update() -> void; - - private: - std::thread periodic_updater(); - std::string getTag(mpd_tag_type type, unsigned idx = 0); - void setLabel(); - std::string getStateIcon(); - std::string getOptionIcon(std::string optionName, bool activated); - - std::thread event_listener(); - - // Assumes `connection_lock_` is locked - void tryConnect(); - // If checking errors on the main connection, make sure to lock it using - // `connection_lock_` before calling checkErrors - void checkErrors(mpd_connection* conn); - - // Assumes `connection_lock_` is locked - void fetchState(); - void waitForEvent(); - - bool handlePlayPause(GdkEventButton* const&); - - bool stopped(); - bool playing(); - bool paused(); - - const std::string module_name_; - - using unique_connection = std::unique_ptr; - using unique_status = std::unique_ptr; - using unique_song = std::unique_ptr; - - // Not using unique_ptr since we don't manage the pointer - // (It's either nullptr, or from the config) - const char* server_; - const unsigned port_; - - unsigned timeout_; - - // We need a mutex here because we can trigger updates from multiple thread: - // the event based updates, the periodic updates needed for the elapsed time, - // and the click play/pause feature - std::mutex connection_lock_; - unique_connection connection_; - // The alternate connection will be used to wait for events: since it will - // be blocking (idle) we can't send commands via this connection - // - // No lock since only used in the event listener thread - unique_connection alternate_connection_; - - // Protect them using the `connection_lock_` - unique_status status_; - mpd_state state_; - unique_song song_; - - // To make sure the previous periodic_updater stops before creating a new one - std::mutex periodic_lock_; -}; - -} // namespace waybar::modules diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp new file mode 100644 index 00000000..0fc1ce99 --- /dev/null +++ b/include/modules/mpd/mpd.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "ALabel.hpp" +#include "modules/mpd/state.hpp" + +namespace waybar::modules { + +class MPD : public ALabel { + friend class detail::Context; + + // State machine + detail::Context context_{this}; + + const std::string module_name_; + + // Not using unique_ptr since we don't manage the pointer + // (It's either nullptr, or from the config) + const char* server_; + const unsigned port_; + const std::string password_; + + unsigned timeout_; + + detail::unique_connection connection_; + + detail::unique_status status_; + mpd_state state_; + detail::unique_song song_; + + public: + MPD(const std::string&, const Json::Value&); + virtual ~MPD() noexcept = default; + auto update() -> void; + + private: + std::string getTag(mpd_tag_type type, unsigned idx = 0) const; + void setLabel(); + std::string getStateIcon() const; + std::string getOptionIcon(std::string optionName, bool activated) const; + + // GUI-side methods + bool handlePlayPause(GdkEventButton* const&); + void emit() { dp.emit(); } + + // MPD-side, Non-GUI methods. + void tryConnect(); + void checkErrors(mpd_connection* conn); + void fetchState(); + void queryMPD(); + + inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } + inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } + inline bool paused() const { return connection_ && state_ == MPD_STATE_PAUSE; } +}; + +#if !defined(MPD_NOINLINE) +#include "modules/mpd/state.inl.hpp" +#endif + +} // namespace waybar::modules diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp new file mode 100644 index 00000000..3b181598 --- /dev/null +++ b/include/modules/mpd/state.hpp @@ -0,0 +1,217 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "ALabel.hpp" + +namespace waybar::modules { +class MPD; +} // namespace waybar::modules + +namespace waybar::modules::detail { + +using unique_connection = std::unique_ptr; +using unique_status = std::unique_ptr; +using unique_song = std::unique_ptr; + +class Context; + +/// This state machine loosely follows a non-hierarchical, statechart +/// pattern, and includes ENTRY and EXIT actions. +/// +/// The State class is the base class for all other states. The +/// entry and exit methods are automatically called when entering +/// into a new state and exiting from the current state. This +/// includes initially entering (Disconnected class) and exiting +/// Waybar. +/// +/// The following nested "top-level" states are represented: +/// 1. Idle - await notification of MPD activity. +/// 2. All Non-Idle states: +/// 1. Playing - An active song is producing audio output. +/// 2. Paused - The current song is paused. +/// 3. Stopped - No song is actively playing. +/// 3. Disconnected - periodically attempt MPD (re-)connection. +/// +/// NOTE: Since this statechart is non-hierarchical, the above +/// states are flattened into a set. + +class State { + public: + virtual ~State() noexcept = default; + + virtual void entry() noexcept { spdlog::debug("mpd: ignore entry action"); } + virtual void exit() noexcept { spdlog::debug("mpd: ignore exit action"); } + + virtual void play() { spdlog::debug("mpd: ignore play state transition"); } + virtual void stop() { spdlog::debug("mpd: ignore stop state transition"); } + virtual void pause() { spdlog::debug("mpd: ignore pause state transition"); } + + /// Request state update the GUI. + virtual void update() noexcept { spdlog::debug("mpd: ignoring update method request"); } +}; + +class Idle : public State { + Context* const ctx_; + sigc::connection idle_connection_; + + public: + Idle(Context* const ctx) : ctx_{ctx} {} + virtual ~Idle() noexcept { this->exit(); }; + + void entry() noexcept override; + void exit() noexcept override; + + void play() override; + void stop() override; + void pause() override; + void update() noexcept override; + + private: + Idle(const Idle&) = delete; + Idle& operator=(const Idle&) = delete; + + bool on_io(Glib::IOCondition const&); +}; + +class Playing : public State { + Context* const ctx_; + sigc::connection timer_connection_; + + public: + Playing(Context* const ctx) : ctx_{ctx} {} + virtual ~Playing() noexcept { this->exit(); } + + void entry() noexcept override; + void exit() noexcept override; + + void pause() override; + void stop() override; + void update() noexcept override; + + private: + Playing(Playing const&) = delete; + Playing& operator=(Playing const&) = delete; + + bool on_timer(); +}; + +class Paused : public State { + Context* const ctx_; + sigc::connection timer_connection_; + + public: + Paused(Context* const ctx) : ctx_{ctx} {} + virtual ~Paused() noexcept { this->exit(); } + + void entry() noexcept override; + void exit() noexcept override; + + void play() override; + void stop() override; + void update() noexcept override; + + private: + Paused(Paused const&) = delete; + Paused& operator=(Paused const&) = delete; + + bool on_timer(); +}; + +class Stopped : public State { + Context* const ctx_; + sigc::connection timer_connection_; + + public: + Stopped(Context* const ctx) : ctx_{ctx} {} + virtual ~Stopped() noexcept { this->exit(); } + + void entry() noexcept override; + void exit() noexcept override; + + void play() override; + void pause() override; + void update() noexcept override; + + private: + Stopped(Stopped const&) = delete; + Stopped& operator=(Stopped const&) = delete; + + bool on_timer(); +}; + +class Disconnected : public State { + Context* const ctx_; + sigc::connection timer_connection_; + + public: + Disconnected(Context* const ctx) : ctx_{ctx} {} + virtual ~Disconnected() noexcept { this->exit(); } + + void entry() noexcept override; + void exit() noexcept override; + + void update() noexcept override; + + private: + Disconnected(Disconnected const&) = delete; + Disconnected& operator=(Disconnected const&) = delete; + + void arm_timer(int interval) noexcept; + void disarm_timer() noexcept; + + bool on_timer(); +}; + +class Context { + std::unique_ptr state_; + waybar::modules::MPD* mpd_module_; + + friend class State; + friend class Playing; + friend class Paused; + friend class Stopped; + friend class Disconnected; + friend class Idle; + + protected: + void setState(std::unique_ptr&& new_state) noexcept { + if (state_.get() != nullptr) { + state_->exit(); + } + state_ = std::move(new_state); + state_->entry(); + } + + bool is_connected() const; + bool is_playing() const; + bool is_paused() const; + bool is_stopped() const; + constexpr std::size_t interval() const; + void tryConnect() const; + void checkErrors(mpd_connection*) const; + void do_update(); + void queryMPD() const; + void fetchState() const; + constexpr mpd_state state() const; + void emit() const; + [[nodiscard]] unique_connection& connection(); + + public: + explicit Context(waybar::modules::MPD* const mpd_module) + : state_{std::make_unique(this)}, mpd_module_{mpd_module} { + state_->entry(); + } + + void play() { state_->play(); } + void stop() { state_->stop(); } + void pause() { state_->pause(); } + void update() noexcept { state_->update(); } +}; + +} // namespace waybar::modules::detail diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp new file mode 100644 index 00000000..0d83b0b3 --- /dev/null +++ b/include/modules/mpd/state.inl.hpp @@ -0,0 +1,24 @@ +#pragma once + +namespace detail { + +inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; } +inline bool Context::is_playing() const { return mpd_module_->playing(); } +inline bool Context::is_paused() const { return mpd_module_->paused(); } +inline bool Context::is_stopped() const { return mpd_module_->stopped(); } + +constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } +inline void Context::tryConnect() const { mpd_module_->tryConnect(); } +inline unique_connection& Context::connection() { return mpd_module_->connection_; } +constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } + +inline void Context::do_update() { + mpd_module_->setLabel(); +} + +inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } +inline void Context::queryMPD() const { mpd_module_->queryMPD(); } +inline void Context::fetchState() const { mpd_module_->fetchState(); } +inline void Context::emit() const { mpd_module_->emit(); } + +} // namespace detail diff --git a/include/modules/network.hpp b/include/modules/network.hpp index edb5aa68..c91b598b 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -2,16 +2,16 @@ #include #include -#include #include -#include #include #include #include #include #include "ALabel.hpp" #include "util/sleeper_thread.hpp" +#ifdef WANT_RFKILL #include "util/rfkill.hpp" +#endif namespace waybar::modules { @@ -26,26 +26,24 @@ class Network : public ALabel { static const uint8_t EPOLL_MAX = 200; static int handleEvents(struct nl_msg*, void*); + static int handleEventsDone(struct nl_msg*, void*); static int handleScan(struct nl_msg*, void*); + void askForStateDump(void); + void worker(); void createInfoSocket(); void createEventSocket(); - int getExternalInterface(int skip_idx = -1) const; - void getInterfaceAddress(); - int netlinkRequest(void*, uint32_t, uint32_t groups = 0) const; - int netlinkResponse(void*, uint32_t, uint32_t groups = 0) const; void parseEssid(struct nlattr**); void parseSignal(struct nlattr**); void parseFreq(struct nlattr**); bool associatedOrJoined(struct nlattr**); - bool checkInterface(struct ifinfomsg* rtif, std::string name); - int getPreferredIface(int skip_idx = -1, bool wait = true) const; + bool checkInterface(std::string name); auto getInfo() -> void; - void checkNewInterface(struct ifinfomsg* rtif); const std::string getNetworkState() const; void clearIface(); bool wildcardMatch(const std::string& pattern, const std::string& text) const; + std::optional> readBandwidthUsage(); int ifid_; sa_family_t family_; @@ -57,24 +55,32 @@ class Network : public ALabel { int nl80211_id_; std::mutex mutex_; + bool want_route_dump_; + bool want_link_dump_; + bool want_addr_dump_; + bool dump_in_progress_; + unsigned long long bandwidth_down_total_; unsigned long long bandwidth_up_total_; std::string state_; std::string essid_; + bool carrier_; std::string ifname_; std::string ipaddr_; + std::string gwaddr_; std::string netmask_; int cidr_; int32_t signal_strength_dbm_; uint8_t signal_strength_; uint32_t frequency_; + uint32_t route_priority; util::SleeperThread thread_; util::SleeperThread thread_timer_; - util::SleeperThread thread_rfkill_; - +#ifdef WANT_RFKILL util::Rfkill rfkill_; +#endif }; } // namespace waybar::modules diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp index 5f17620a..99511b37 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -24,7 +24,7 @@ class Pulseaudio : public ALabel { static void volumeModifyCb(pa_context*, int, void*); bool handleScroll(GdkEventScroll* e); - const std::string getPortIcon() const; + const std::vector getPulseIcon() const; pa_threaded_mainloop* mainloop_; pa_mainloop_api* mainloop_api_; @@ -38,7 +38,8 @@ class Pulseaudio : public ALabel { std::string form_factor_; std::string desc_; std::string monitor_; - std::string default_sink_name_; + std::string current_sink_name_; + bool current_sink_running_; // SOURCE uint32_t source_idx_{0}; uint16_t source_volume_; diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp new file mode 100644 index 00000000..c49ec60d --- /dev/null +++ b/include/modules/river/tags.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "river-status-unstable-v1-client-protocol.h" +#include "river-control-unstable-v1-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace waybar::modules::river { + +class Tags : public waybar::AModule { + public: + Tags(const std::string &, const waybar::Bar &, const Json::Value &); + ~Tags(); + + // Handlers for wayland events + void handle_focused_tags(uint32_t tags); + void handle_view_tags(struct wl_array *tags); + void handle_urgent_tags(uint32_t tags); + + void handle_primary_clicked(uint32_t tag); + bool handle_button_press(GdkEventButton *event_button, uint32_t tag); + + struct zriver_status_manager_v1 *status_manager_; + struct zriver_control_v1 *control_; + struct wl_seat *seat_; + + private: + const waybar::Bar & bar_; + Gtk::Box box_; + std::vector buttons_; + struct zriver_output_status_v1 *output_status_; +}; + +} /* namespace waybar::modules::river */ diff --git a/include/modules/simpleclock.hpp b/include/modules/simpleclock.hpp new file mode 100644 index 00000000..aa9a0a22 --- /dev/null +++ b/include/modules/simpleclock.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#if FMT_VERSION < 60000 +#include +#else +#include +#endif +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Clock : public ALabel { + public: + Clock(const std::string&, const Json::Value&); + ~Clock() = default; + auto update() -> void; + + private: + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/sndio.hpp b/include/modules/sndio.hpp new file mode 100644 index 00000000..32ed7066 --- /dev/null +++ b/include/modules/sndio.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Sndio : public ALabel { + public: + Sndio(const std::string&, const Json::Value&); + ~Sndio(); + auto update() -> void; + auto set_desc(struct sioctl_desc *, unsigned int) -> void; + auto put_val(unsigned int, unsigned int) -> void; + bool handleScroll(GdkEventScroll *); + bool handleToggle(GdkEventButton* const&); + + private: + auto connect_to_sndio() -> void; + util::SleeperThread thread_; + struct sioctl_hdl *hdl_; + std::vector pfds_; + unsigned int addr_; + unsigned int volume_, old_volume_, maxval_; + bool muted_; +}; + +} // namespace waybar::modules diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index f97900fd..8d321036 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -5,13 +5,15 @@ #include #include #include +#include "bar.hpp" #include "modules/sni/item.hpp" namespace waybar::modules::SNI { class Host { public: - Host(const std::size_t id, const Json::Value&, const std::function&)>&, + Host(const std::size_t id, const Json::Value&, const Bar&, + const std::function&)>&, const std::function&)>&); ~Host(); @@ -36,6 +38,7 @@ class Host { GCancellable* cancellable_ = nullptr; SnWatcher* watcher_ = nullptr; const Json::Value& config_; + const Bar& bar_; const std::function&)> on_add_; const std::function&)> on_remove_; }; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 3cbd0b74..430c351c 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -11,11 +11,21 @@ #include #include +#include +#include + +#include "bar.hpp" + namespace waybar::modules::SNI { +struct ToolTip { + Glib::ustring icon_name; + Glib::ustring text; +}; + class Item : public sigc::trackable { public: - Item(const std::string&, const std::string&, const Json::Value&); + Item(const std::string&, const std::string&, const Json::Value&, const Bar&); ~Item() = default; std::string bus_name; @@ -27,10 +37,8 @@ class Item : public sigc::trackable { Gtk::EventBox event_box; std::string category; std::string id; - std::string status; std::string title; - int32_t window_id; std::string icon_name; Glib::RefPtr icon_pixmap; Glib::RefPtr icon_theme; @@ -39,6 +47,7 @@ class Item : public sigc::trackable { std::string attention_movie_name; std::string icon_theme_path; std::string menu; + ToolTip tooltip; DbusmenuGtkMenu* dbus_menu = nullptr; Gtk::Menu* gtk_menu = nullptr; /** @@ -49,8 +58,10 @@ class Item : public sigc::trackable { bool item_is_menu = true; private: + void onConfigure(GdkEventConfigure* ev); void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); + void setStatus(const Glib::ustring& value); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); void onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, @@ -58,14 +69,24 @@ class Item : public sigc::trackable { void updateImage(); Glib::RefPtr extractPixBuf(GVariant* variant); + Glib::RefPtr getIconPixbuf(); Glib::RefPtr getIconByName(const std::string& name, int size); + double getScaledIconSize(); static void onMenuDestroyed(Item* self, GObject* old_menu_pointer); void makeMenu(); bool handleClick(GdkEventButton* const& /*ev*/); + bool handleScroll(GdkEventScroll* const&); + + // smooth scrolling threshold + gdouble scroll_threshold_ = 0; + gdouble distance_scrolled_x_ = 0; + gdouble distance_scrolled_y_ = 0; + // visibility of items with Status == Passive + bool show_passive_ = false; Glib::RefPtr proxy_; Glib::RefPtr cancellable_; - bool update_pending_; + std::set update_pending_; }; } // namespace waybar::modules::SNI diff --git a/include/modules/sway/ipc/ipc.hpp b/include/modules/sway/ipc/ipc.hpp index 2c5a7a6e..5f23d172 100644 --- a/include/modules/sway/ipc/ipc.hpp +++ b/include/modules/sway/ipc/ipc.hpp @@ -29,4 +29,8 @@ enum ipc_command_type { IPC_EVENT_BINDING = ((1 << 31) | 5), IPC_EVENT_SHUTDOWN = ((1 << 31) | 6), IPC_EVENT_TICK = ((1 << 31) | 7), + + // sway-specific event types + IPC_EVENT_BAR_STATE_UPDATE = ((1<<31) | 20), + IPC_EVENT_INPUT = ((1<<31) | 21), }; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp new file mode 100644 index 00000000..1faf52b3 --- /dev/null +++ b/include/modules/sway/language.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "client.hpp" +#include "modules/sway/ipc/client.hpp" +#include "util/json.hpp" + +namespace waybar::modules::sway { + +class Language : public ALabel, public sigc::trackable { + public: + Language(const std::string& id, const Json::Value& config); + ~Language() = default; + auto update() -> void; + + private: + enum class DispayedShortFlag { + None = 0, + ShortName = 1, + ShortDescription = 1 << 1 + }; + + struct Layout { + std::string full_name; + std::string short_name; + std::string variant; + std::string short_description; + }; + + class XKBContext { + public: + XKBContext(); + ~XKBContext(); + auto next_layout() -> Layout*; + private: + rxkb_context* context_ = nullptr; + rxkb_layout* xkb_layout_ = nullptr; + Layout* layout_ = nullptr; + std::map base_layouts_by_name_; + }; + + void onEvent(const struct Ipc::ipc_response&); + void onCmd(const struct Ipc::ipc_response&); + + auto set_current_layout(std::string current_layout) -> void; + auto init_layouts_map(const std::vector& used_layouts) -> void; + + const static std::string XKB_LAYOUT_NAMES_KEY; + const static std::string XKB_ACTIVE_LAYOUT_NAME_KEY; + + Layout layout_; + std::string tooltip_format_ = ""; + std::map layouts_map_; + bool is_variant_displayed; + std::byte displayed_short_flag = static_cast(DispayedShortFlag::None); + + util::JsonParser parser_; + std::mutex mutex_; + Ipc ipc_; +}; + +} // namespace waybar::modules::sway diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index 40aaa1a0..0f7ae317 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -22,6 +22,7 @@ class Window : public ALabel, public sigc::trackable { std::tuple getFocusedNode(const Json::Value& nodes, std::string& output); void getTree(); + std::string rewriteTitle(const std::string& title); const Bar& bar_; std::string window_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 5454b9e3..c6443836 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -20,6 +20,10 @@ class Workspaces : public AModule, public sigc::trackable { auto update() -> void; private: + static inline const std::string workspace_switch_cmd_ = "workspace {} \"{}\""; + + static int convertWorkspaceNameToNum(std::string name); + void onCmd(const struct Ipc::ipc_response&); void onEvent(const struct Ipc::ipc_response&); bool filterButtons(); diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp new file mode 100644 index 00000000..3fe032f8 --- /dev/null +++ b/include/modules/wlr/taskbar.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include "AModule.hpp" +#include "bar.hpp" +#include "client.hpp" +#include "util/json.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + + +namespace waybar::modules::wlr { + +class Taskbar; + +class Task +{ + public: + Task(const waybar::Bar&, const Json::Value&, Taskbar*, + struct zwlr_foreign_toplevel_handle_v1 *, struct wl_seat*); + ~Task(); + + public: + enum State { + MAXIMIZED = (1 << 0), + MINIMIZED = (1 << 1), + ACTIVE = (1 << 2), + FULLSCREEN = (1 << 3), + INVALID = (1 << 4) + }; + + private: + static uint32_t global_id; + + private: + const waybar::Bar &bar_; + const Json::Value &config_; + Taskbar *tbar_; + struct zwlr_foreign_toplevel_handle_v1 *handle_; + struct wl_seat *seat_; + + uint32_t id_; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Image icon_; + Gtk::Label text_before_; + Gtk::Label text_after_; + bool button_visible_ = false; + bool ignored_ = false; + + bool with_icon_; + std::string format_before_; + std::string format_after_; + + std::string format_tooltip_; + + std::string title_; + std::string app_id_; + uint32_t state_ = 0; + + private: + std::string repr() const; + std::string state_string(bool = false) const; + void hide_if_ignored(); + + public: + /* Getter functions */ + uint32_t id() const { return id_; } + std::string title() const { return title_; } + std::string app_id() const { return app_id_; } + uint32_t state() const { return state_; } + bool maximized() const { return state_ & MAXIMIZED; } + bool minimized() const { return state_ & MINIMIZED; } + bool active() const { return state_ & ACTIVE; } + bool fullscreen() const { return state_ & FULLSCREEN; } + + public: + /* Callbacks for the wlr protocol */ + void handle_title(const char *); + void handle_app_id(const char *); + void handle_output_enter(struct wl_output *); + void handle_output_leave(struct wl_output *); + void handle_state(struct wl_array *); + void handle_done(); + void handle_closed(); + + /* Callbacks for Gtk events */ + bool handle_clicked(GdkEventButton *); + + public: + bool operator==(const Task&) const; + bool operator!=(const Task&) const; + + public: + void update(); + + public: + /* Interaction with the tasks */ + void maximize(bool); + void minimize(bool); + void activate(); + void fullscreen(bool); + void close(); +}; + +using TaskPtr = std::unique_ptr; + + +class Taskbar : public waybar::AModule +{ + public: + Taskbar(const std::string&, const waybar::Bar&, const Json::Value&); + ~Taskbar(); + void update(); + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector tasks_; + + std::vector> icon_themes_; + std::unordered_set ignore_list_; + + struct zwlr_foreign_toplevel_manager_v1 *manager_; + struct wl_seat *seat_; + + public: + /* Callbacks for global registration */ + void register_manager(struct wl_registry*, uint32_t name, uint32_t version); + void register_seat(struct wl_registry*, uint32_t name, uint32_t version); + + /* Callbacks for the wlr protocol */ + void handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1 *); + void handle_finished(); + + public: + void add_button(Gtk::Button &); + void move_button(Gtk::Button &, int); + void remove_button(Gtk::Button &); + void remove_task(uint32_t); + + bool show_output(struct wl_output *) const; + bool all_outputs() const; + + std::vector> icon_themes() const; + const std::unordered_set& ignore_list() const; +}; + +} /* namespace waybar::modules::wlr */ diff --git a/include/modules/wlr/workspace_manager.hpp b/include/modules/wlr/workspace_manager.hpp new file mode 100644 index 00000000..e4cdb4da --- /dev/null +++ b/include/modules/wlr/workspace_manager.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "ext-workspace-unstable-v1-client-protocol.h" + +namespace waybar::modules::wlr { + +class WorkspaceManager; +class WorkspaceGroup; + +class Workspace { + public: + Workspace(const waybar::Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, + zext_workspace_handle_v1 *workspace, uint32_t id); + ~Workspace(); + auto update() -> void; + + auto id() const -> uint32_t { return id_; } + auto is_active() const -> bool { return state_ & static_cast(State::ACTIVE); } + auto is_urgent() const -> bool { return state_ & static_cast(State::URGENT); } + auto is_hidden() const -> bool { return state_ & static_cast(State::HIDDEN); } + // wlr stuff + auto handle_name(const std::string &name) -> void; + auto handle_coordinates(const std::vector &coordinates) -> void; + auto handle_state(const std::vector &state) -> void; + auto handle_remove() -> void; + + auto handle_done() -> void; + auto handle_clicked(GdkEventButton *bt) -> bool; + auto show() -> void; + auto hide() -> void; + auto get_button_ref() -> Gtk::Button & { return button_; } + auto get_name() -> std::string & { return name_; } + auto get_coords() -> std::vector & { return coordinates_; } + + enum class State { + ACTIVE = (1 << 0), + URGENT = (1 << 1), + HIDDEN = (1 << 2), + }; + + private: + auto get_icon() -> std::string; + + const Bar &bar_; + const Json::Value &config_; + WorkspaceGroup &workspace_group_; + + // wlr stuff + zext_workspace_handle_v1 *workspace_handle_; + uint32_t state_ = 0; + + uint32_t id_; + std::string name_; + std::vector coordinates_; + static std::map icons_map_; + std::string format_; + bool with_icon_ = false; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Label label_; +}; + +class WorkspaceGroup { + public: + WorkspaceGroup(const waybar::Bar &bar, Gtk::Box &box, const Json::Value &config, + WorkspaceManager &manager, zext_workspace_group_handle_v1 *workspace_group_handle, + uint32_t id); + ~WorkspaceGroup(); + auto update() -> void; + + auto id() const -> uint32_t { return id_; } + auto is_visible() const -> bool; + auto remove_workspace(uint32_t id_) -> void; + auto active_only() const -> bool; + auto creation_delayed() const -> bool; + auto workspaces() -> std::vector> & { return workspaces_; } + + auto sort_workspaces() -> void; + auto set_need_to_sort() -> void { need_to_sort = true; } + auto add_button(Gtk::Button &button) -> void; + auto remove_button(Gtk::Button &button) -> void; + + // wlr stuff + auto handle_workspace_create(zext_workspace_handle_v1 *workspace_handle) -> void; + auto handle_remove() -> void; + auto handle_output_enter(wl_output *output) -> void; + auto handle_output_leave() -> void; + auto handle_done() -> void; + auto commit() -> void; + + private: + static uint32_t workspace_global_id; + const waybar::Bar &bar_; + Gtk::Box &box_; + const Json::Value &config_; + WorkspaceManager &workspace_manager_; + + // wlr stuff + zext_workspace_group_handle_v1 *workspace_group_handle_; + wl_output *output_ = nullptr; + + uint32_t id_; + std::vector> workspaces_; + bool need_to_sort = false; +}; + +class WorkspaceManager : public AModule { + public: + WorkspaceManager(const std::string &id, const waybar::Bar &bar, const Json::Value &config); + ~WorkspaceManager() override; + auto update() -> void override; + + auto all_outputs() const -> bool { return all_outputs_; } + auto active_only() const -> bool { return active_only_; } + auto workspace_comparator() const + -> std::function &, std::unique_ptr &)>; + auto creation_delayed() const -> bool { return creation_delayed_; } + + auto sort_workspaces() -> void; + auto remove_workspace_group(uint32_t id_) -> void; + + // wlr stuff + auto register_manager(wl_registry *registry, uint32_t name, uint32_t version) -> void; + auto handle_workspace_group_create(zext_workspace_group_handle_v1 *workspace_group_handle) + -> void; + auto handle_done() -> void; + auto handle_finished() -> void; + auto commit() -> void; + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector> groups_; + + // wlr stuff + zext_workspace_manager_v1 *workspace_manager_ = nullptr; + + static uint32_t group_global_id; + + bool sort_by_name_ = true; + bool sort_by_coordinates_ = true; + bool all_outputs_ = false; + bool active_only_ = false; + bool creation_delayed_ = false; +}; + +} // namespace waybar::modules::wlr diff --git a/include/modules/wlr/workspace_manager_binding.hpp b/include/modules/wlr/workspace_manager_binding.hpp new file mode 100644 index 00000000..0bfe663f --- /dev/null +++ b/include/modules/wlr/workspace_manager_binding.hpp @@ -0,0 +1,8 @@ +#include "ext-workspace-unstable-v1-client-protocol.h" + +namespace waybar::modules::wlr { + void add_registry_listener(void *data); + void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data); + void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle, void *data); + zext_workspace_manager_v1* workspace_manager_bind(wl_registry *registry, uint32_t name, uint32_t version, void *data); +} diff --git a/include/util/command.hpp b/include/util/command.hpp index a72a8294..3a38da36 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -5,8 +5,18 @@ #include #include +#ifdef __linux__ +#include +#endif +#ifdef __FreeBSD__ +#include +#endif + #include +extern std::mutex reap_mtx; +extern std::list reap; + namespace waybar::util::command { struct res { @@ -32,10 +42,11 @@ inline std::string read(FILE* fp) { inline int close(FILE* fp, pid_t pid) { int stat = -1; + pid_t ret; fclose(fp); do { - waitpid(pid, &stat, WCONTINUED | WUNTRACED); + ret = waitpid(pid, &stat, WCONTINUED | WUNTRACED); if (WIFEXITED(stat)) { spdlog::debug("Cmd exited with code {}", WEXITSTATUS(stat)); @@ -45,6 +56,8 @@ inline int close(FILE* fp, pid_t pid) { spdlog::debug("Cmd stopped by {}", WSTOPSIG(stat)); } else if (WIFCONTINUED(stat)) { spdlog::debug("Cmd continued"); + } else if (ret == -1) { + spdlog::debug("waitpid failed: {}", strerror(errno)); } else { break; } @@ -65,6 +78,24 @@ inline FILE* open(const std::string& cmd, int& pid) { } if (!child_pid) { + int err; + sigset_t mask; + sigfillset(&mask); + // Reset sigmask + err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); + if (err != 0) spdlog::error("pthread_sigmask in open failed: {}", strerror(err)); + // Kill child if Waybar exits + int deathsig = SIGTERM; +#ifdef __linux__ + if (prctl(PR_SET_PDEATHSIG, deathsig) != 0) { + spdlog::error("prctl(PR_SET_PDEATHSIG) in open failed: {}", strerror(errno)); + } +#endif +#ifdef __FreeBSD__ + if (procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)) == -1) { + spdlog::error("procctl(PROC_PDEATHSIG_CTL) in open failed: {}", strerror(errno)); + } +#endif ::close(fd[0]); dup2(fd[1], 1); setpgid(child_pid, child_pid); @@ -97,7 +128,7 @@ inline struct res execNoRead(const std::string& cmd) { inline int32_t forkExec(const std::string& cmd) { if (cmd == "") return -1; - int32_t pid = fork(); + pid_t pid = fork(); if (pid < 0) { spdlog::error("Unable to exec cmd {}, error {}", cmd.c_str(), strerror(errno)); @@ -106,12 +137,20 @@ inline int32_t forkExec(const std::string& cmd) { // Child executes the command if (!pid) { + int err; + sigset_t mask; + sigfillset(&mask); + // Reset sigmask + err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); + if (err != 0) spdlog::error("pthread_sigmask in forkExec failed: {}", strerror(err)); setpgid(pid, pid); - signal(SIGCHLD, SIG_DFL); execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); exit(0); } else { - signal(SIGCHLD, SIG_IGN); + reap_mtx.lock(); + reap.push_back(pid); + reap_mtx.unlock(); + spdlog::debug("Added child to reap list: {}", pid); } return pid; diff --git a/include/util/format.hpp b/include/util/format.hpp index 0147701b..543a100f 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -23,7 +23,7 @@ namespace fmt { constexpr auto parse(ParseContext& ctx) -> decltype (ctx.begin()) { auto it = ctx.begin(), end = ctx.end(); if (it != end && *it == ':') ++it; - if (*it == '>' || *it == '<' || *it == '=') { + if (it && (*it == '>' || *it == '<' || *it == '=')) { spec = *it; ++it; } @@ -35,7 +35,11 @@ namespace fmt { // The rationale for ignoring it is that the only reason to specify // an alignment and a with is to get a fixed width bar, and ">" is // sufficient in this implementation. +#if FMT_VERSION < 80000 width = parse_nonnegative_int(it, end, ctx); +#else + width = detail::parse_nonnegative_int(it, end, -1); +#endif } return it; } diff --git a/include/util/rfkill.hpp b/include/util/rfkill.hpp index 5dbf3ced..5d519cae 100644 --- a/include/util/rfkill.hpp +++ b/include/util/rfkill.hpp @@ -1,19 +1,26 @@ #pragma once +#include #include +#include +#include namespace waybar::util { -class Rfkill { - public:; +class Rfkill : public sigc::trackable { + public: Rfkill(enum rfkill_type rfkill_type); - ~Rfkill() = default; - void waitForEvent(); + ~Rfkill(); bool getState() const; + sigc::signal on_update; + private: enum rfkill_type rfkill_type_; - int state_ = 0; + bool state_ = false; + int fd_ = -1; + + bool on_event(Glib::IOCondition cond); }; } // namespace waybar::util diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 642d47d4..d1c6ba0d 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -8,6 +8,20 @@ namespace waybar::util { +/** + * Defer pthread_cancel until the end of a current scope. + * + * Required to protect a scope where it's unsafe to raise `__forced_unwind` exception. + * An example of these is a call of a method marked as `noexcept`; an attempt to cancel within such + * a method may result in a `std::terminate` call. + */ +class CancellationGuard { + int oldstate; +public: + CancellationGuard() { pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate); } + ~CancellationGuard() { pthread_setcancelstate(oldstate, &oldstate); } +}; + class SleeperThread { public: SleeperThread() = default; @@ -33,14 +47,16 @@ class SleeperThread { bool isRunning() const { return do_run_; } auto sleep_for(std::chrono::system_clock::duration dur) { - std::unique_lock lk(mutex_); + std::unique_lock lk(mutex_); + CancellationGuard cancel_lock; return condvar_.wait_for(lk, dur, [this] { return signal_ || !do_run_; }); } auto sleep_until( std::chrono::time_point time_point) { - std::unique_lock lk(mutex_); + std::unique_lock lk(mutex_); + CancellationGuard cancel_lock; return condvar_.wait_until(lk, time_point, [this] { return signal_ || !do_run_; }); } @@ -59,6 +75,11 @@ class SleeperThread { do_run_ = false; } condvar_.notify_all(); + auto handle = thread_.native_handle(); + if (handle != 0) { + // TODO: find a proper way to terminate thread... + pthread_cancel(handle); + } } ~SleeperThread() { diff --git a/include/util/string.hpp b/include/util/string.hpp new file mode 100644 index 00000000..d644b4c4 --- /dev/null +++ b/include/util/string.hpp @@ -0,0 +1,15 @@ +#include + +const std::string WHITESPACE = " \n\r\t\f\v"; + +std::string ltrim(const std::string s) { + size_t begin = s.find_first_not_of(WHITESPACE); + return (begin == std::string::npos) ? "" : s.substr(begin); +} + +std::string rtrim(const std::string s) { + size_t end = s.find_last_not_of(WHITESPACE); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +std::string trim(const std::string& s) { return rtrim(ltrim(s)); } diff --git a/include/util/ustring_clen.hpp b/include/util/ustring_clen.hpp new file mode 100644 index 00000000..cddd2e1a --- /dev/null +++ b/include/util/ustring_clen.hpp @@ -0,0 +1,5 @@ +#pragma once +#include + +// calculate column width of ustring +int ustring_clen(const Glib::ustring &str); \ No newline at end of file diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index e6116e3e..d14e4f24 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -24,6 +24,14 @@ The *backlight* module displays the current backlight level. 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. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 75d41081..e8053c91 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -20,7 +20,12 @@ The *battery* module displays the current capacity and state (eg. charging) of y *full-at*: ++ typeof: integer ++ - Define the max percentage of the battery, usefull for an old battery, e.g. 96 + 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. *interval*: ++ typeof: integer ++ @@ -50,6 +55,14 @@ The *battery* module displays the current capacity and state (eg. charging) of y typeof: integer++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *rotate*: ++ typeof: integer++ Positive value to rotate the text label. @@ -91,6 +104,8 @@ The *battery* module displays the current capacity and state (eg. charging) of y *{capacity}*: Capacity in percentage +*{power}*: Power in watts + *{icon}*: Icon, as defined in *format-icons*. *{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. @@ -105,7 +120,7 @@ The two arguments are: # CUSTOM FORMATS -The *battery* module allows 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. diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index 8151ec01..d4ecb1d1 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -12,11 +12,6 @@ The *bluetooth* module displays information about the status of the device's blu Addressed by *bluetooth* -*interval*: ++ - typeof: integer ++ - default: 60 ++ - The interval in which the bluetooth state gets updated. - *format*: ++ typeof: string ++ default: *{icon}* ++ @@ -35,6 +30,14 @@ Addressed by *bluetooth* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -80,12 +83,11 @@ Addressed by *bluetooth* "bluetooth": { "format": "{icon}", "format-alt": "bluetooth: {status}", - "interval": 30, "format-icons": { "enabled": "", "disabled": "" }, - "tooltip-format": "{status}" + "tooltip-format": "{}" } ``` diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 3610f19d..2c901d2d 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -24,17 +24,36 @@ The *clock* module displays the current date and time. *timezone*: ++ typeof: string ++ default: inferred local timezone ++ - The timezone to display the time in, e.g. America/New_York. + The timezone to display the time in, e.g. America/New_York. ++ + This field will be ignored if *timezones* field is set and have at least one value. + +*timezones*: ++ + typeof: list of strings ++ + A list of timezones to use for time display, changed using the scroll wheel. ++ + Use "" to represent the system's local timezone. Using %Z in the format or tooltip format is useful to track which time zone is currently displayed. *locale*: ++ typeof: string ++ default: inferred from current locale ++ A locale to be used to display the time. Intended to render times in custom timezones with the proper language and format. +*today-format*: ++ + typeof: string ++ + default: {} ++ + The format of today's date in the calendar. + *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index cb83134f..2e0d6c71 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -20,10 +20,23 @@ The *cpu* module displays the current cpu utilization. default: {usage}% ++ The format, how information should be displayed. On {} data gets inserted. +*format-icons*: ++ + typeof: array/object ++ + Based on the current usage, the corresponding icon gets selected. ++ + The order is *low* to *high*. Or by the state if it is an object. + *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. @@ -69,9 +82,23 @@ The *cpu* module displays the current cpu utilization. *{load}*: Current cpu load. -*{usage}*: Current cpu usage. +*{usage}*: Current overall cpu usage. -# EXAMPLE +*{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. + +*{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. + +*{icon}*: Icon for overall cpu usage. + +*{icon*{n}*}*: Icon for cpu core n usage. Use like {icon0}. + +# EXAMPLES + +Basic configuration: ``` "cpu": { @@ -81,6 +108,16 @@ The *cpu* module displays the current cpu utilization. } ``` +Cpu usage per core rendered as icons: + +``` +"cpu": { + "interval": 1, + "format": "{icon0}{icon1}{icon2}{icon3} {usage:>2}% ", + "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], +}, +``` + # STYLE - *#cpu* diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 121585a9..8f9dcfaf 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -22,6 +22,12 @@ Addressed by *custom/* The path to a script, which determines if the script in *exec* should be executed. *exec* will be executed if the exit code of *exec-if* equals 0. +*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. + *return-type*: ++ typeof: string ++ See *return-type* @@ -61,6 +67,14 @@ Addressed by *custom/* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 1a9320ce..58797141 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -31,10 +31,22 @@ Addressed by *disk* typeof: integer ++ Positive value to rotate the text label. +*states*: ++ + typeof: array ++ + A number of disk utilization states which 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. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 9d231d86..0b0bdd03 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -27,6 +27,14 @@ screensaving, also known as "presentation mode". typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. A click also toggles the state diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd new file mode 100644 index 00000000..1d7c3a83 --- /dev/null +++ b/man/waybar-keyboard-state.5.scd @@ -0,0 +1,80 @@ +waybar-keyboard-state(5) + +# NAME + +waybar - keyboard-state module + +# DESCRIPTION + +The *keyboard-state* module displays the state of number lock, caps lock, and scroll lock. + +# CONFIGURATION + +*interval*: ++ + typeof: integer ++ + default: 1 ++ + The interval, in seconds, to poll the keyboard state. + +*format*: ++ + typeof: string|object ++ + default: {name} {icon} ++ + The format, how information should be displayed. If a string, the same format is used for all keyboard states. If an object, the fields "numlock", "capslock", and "scrolllock" each specify the format for the corresponding state. Any unspecified states use the default format. + +*format-icons*: ++ + typeof: object ++ + default: {"locked": "locked", "unlocked": "unlocked"} ++ + Based on the keyboard state, the corresponding icon gets selected. The same set of icons is used for number, caps, and scroll lock, but the icon is selected from the set independently for each. See *icons*. + +*numlock*: ++ + typeof: bool ++ + default: false ++ + Display the number lock state. + +*capslock*: ++ + typeof: bool ++ + default: false ++ + Display the caps lock state. + +*scrolllock*: ++ + typeof: bool ++ + default: false ++ + Display the scroll lock state. + +*device-path*: ++ + typeof: string ++ + 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. + +# FORMAT REPLACEMENTS + +*{name}*: Caps, Num, or Scroll. + +*{icon}*: Icon, as defined in *format-icons*. + +# ICONS + +The following *format-icons* can be set. + +- *locked*: Will be shown when the keyboard state is locked. Default "locked". +- *unlocked*: Will be shown when the keyboard state is not locked. Default "unlocked" + +# EXAMPLE: + +``` +"keyboard-state": { + "numlock": true, + "capslock": true, + "format": "{name} {icon}", + "format-icons": { + "locked": "", + "unlocked": "" + } +} +``` + +# STYLE + +- *#keyboard-state* +- *#keyboard-state label* +- *#keyboard-state label.locked* + diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 81c62165..0639c07c 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -22,6 +22,11 @@ Addressed by *memory* default: {percentage}% ++ The format, how information should be displayed. +*format-icons*: ++ + typeof: array/object ++ + Based on the current percentage, the corresponding icon gets selected. ++ + The order is *low* to *high*. Or by the state if it is an object. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. @@ -34,6 +39,14 @@ Addressed by *memory* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 1ee7a988..044af98e 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -20,6 +20,10 @@ Addressed by *mpd* typeof: integer ++ The port MPD listens to. If empty, use the default port. +*password*: ++ + typeof: string ++ + The password required to connect to the MPD server. If empty, no password is sent to MPD. + *interval*: ++ typeof: integer++ default: 5 ++ @@ -69,6 +73,22 @@ Addressed by *mpd* default: "MPD (disconnected)" ++ Tooltip information displayed when the MPD server can't be reached. +*artist-len*: ++ + typeof: integer ++ + Maximum length of the Artist tag. + +*album-len*: ++ + typeof: integer ++ + Maximum length of the Album tag. + +*album-artist-len*: ++ + typeof: integer ++ + Maximum length of the Album Artist tag. + +*title-len*: ++ + typeof: integer ++ + Maximum length of the Title tag. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. @@ -77,6 +97,14 @@ Addressed by *mpd* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -144,10 +172,16 @@ Addressed by *mpd* *{date}*: The date of the current song +*{volume}*: The current volume in percent + *{elapsedTime}*: The current position of the current song. To format as a date/time (see example configuration) *{totalTime}*: The length of the current song. To format as a date/time (see example configuration) +*{songPosition}*: The position of the current song. + +*{queueLength}*: The length of the current queue. + *{stateIcon}*: The icon corresponding the playing or paused status of the player (see *state-icons* option) *{consumeIcon}*: The icon corresponding the "consume" option (see *consume-icons* option) diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index aa6eb3f5..f8bdd65d 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -33,7 +33,7 @@ Addressed by *network* *format-ethernet*: ++ typeof: string ++ - This format is used when a ethernet interface is displayed. + This format is used when an ethernet interface is displayed. *format-wifi*: ++ typeof: string ++ @@ -64,6 +64,14 @@ Addressed by *network* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -103,7 +111,7 @@ Addressed by *network* *tooltip-format-ethernet*: ++ typeof: string ++ - This format is used when a ethernet interface is displayed. + This format is used when an ethernet interface is displayed. *tooltip-format-wifi*: ++ typeof: string ++ @@ -123,6 +131,8 @@ Addressed by *network* *{ipaddr}*: The first IP of the interface. +*{gwaddr}*: The default gateway for the interface + *{netmask}*: The subnetmask corresponding to the IP. *{cidr}*: The subnetmask corresponding to the IP in CIDR notation. diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index c3f50e0b..7de10281 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -50,6 +50,14 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *scroll-step*: ++ typeof: float ++ default: 1.0 ++ @@ -101,6 +109,9 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu # ICONS: The following strings for *format-icons* are supported. + +- the device name + If they are found in the current PulseAudio port name, the corresponding icons will be selected. - *default* (Shown, when no other port is found) @@ -123,6 +134,7 @@ If they are found in the current PulseAudio port name, the corresponding icons w "format-bluetooth": "{volume}% {icon}", "format-muted": "", "format-icons": { + "alsa_output.pci-0000_00_1f.3.analog-stereo": "", "headphones": "", "handsfree": "", "headset": "", diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd new file mode 100644 index 00000000..7814ee49 --- /dev/null +++ b/man/waybar-river-tags.5.scd @@ -0,0 +1,49 @@ +waybar-river-tags(5) + +# NAME + +waybar - river tags module + +# DESCRIPTION + +The *tags* module displays the current state of tags in river. + +# CONFIGURATION + +Addressed by *river/tags* + +*num-tags*: ++ + typeof: uint ++ + default: 9 ++ + The number of tags that should be displayed. Max 32. + +*tag-labels*: ++ + typeof: array ++ + The label to display for each tag. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled. + +# EXAMPLE + +``` +"river/tags": { + "num-tags": 5 +} +``` + +# STYLE + +- *#tags button* +- *#tags button.occupied* +- *#tags button.focused* +- *#tags button.urgent* + +Note that occupied/focused/urgent status may overlap. That is, a tag may be +both occupied and focused at the same time. + +# SEE ALSO + +waybar(5), river(1) diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd new file mode 100644 index 00000000..90a73f48 --- /dev/null +++ b/man/waybar-sndio.5.scd @@ -0,0 +1,91 @@ +waybar-sndio(5) + +# NAME + +waybar - sndio module + +# DESCRIPTION + +The *sndio* module displays the current volume reported by sndio(7). + +Additionally, you can control the volume by scrolling *up* or *down* while the +cursor is over the module, and clicking on the module toggles mute. + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {volume}% ++ + The format for how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*scroll-step*: ++ + typeof: int ++ + default: 5 ++ + The speed in which to change the volume when scrolling. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + This replaces the default behaviour of toggling mute. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + This replaces the default behaviour of volume control. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + This replaces the default behaviour of volume control. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +# FORMAT REPLACEMENTS + +*{volume}*: Volume in percentage. + +*{raw_value}*: Volume as value reported by sndio. + +# EXAMPLES + +``` +"sndio": { + "format": "{raw_value} 🎜", + "scroll-step": 3 +} +``` + +# STYLE + +- *#sndio* +- *#sndio.muted* diff --git a/man/waybar-states.5.scd b/man/waybar-states.5.scd index fe6a7307..ae2df1b9 100644 --- a/man/waybar-states.5.scd +++ b/man/waybar-states.5.scd @@ -13,7 +13,7 @@ apply a class when the value matches the declared state value. 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-*. + Those can be configured via *format-*. Or if you want to differentiate a bit more even as *format--*. # EXAMPLE diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd new file mode 100644 index 00000000..92a647e6 --- /dev/null +++ b/man/waybar-sway-language.5.scd @@ -0,0 +1,54 @@ +waybar-sway-language(5) + +# NAME + +waybar - sway language module + +# DESCRIPTION + +The *language* module displays the current keyboard layout in Sway + +# CONFIGURATION + +Addressed by *sway/language* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how layout should be displayed. + +*tooltip-format*: ++ + typeof: string ++ + default: {} ++ + The format, how layout should be displayed in tooltip. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +# 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 + +``` +"sway/language": { + "format": "{}", +}, + +"sway/language": { + "format": "{short} {variant}", +} +``` + +# STYLE + +- *#language* diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 958a1edb..b8b59cd3 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -25,6 +25,14 @@ Addressed by *sway/mode* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 4863a76c..ea060696 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -25,6 +25,14 @@ Addressed by *sway/window* typeof: integer ++ The maximum length in character the module should display. +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -58,12 +66,32 @@ Addressed by *sway/window* default: true ++ Option to disable tooltip on hover. +*rewrite*: ++ + typeof: object ++ + Rules to rewrite window title. See *rewrite rules*. + +# 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 ``` "sway/window": { "format": "{}", - "max-length": 50 + "max-length": 50, + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } } ``` diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 56b703a2..f2808b90 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -31,6 +31,11 @@ Addressed by *sway/workspaces* default: false ++ If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. +*disable-click*: ++ + 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. @@ -61,12 +66,16 @@ Addressed by *sway/workspaces* 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. *numeric-first*: ++ - typeof: bool ++ - Whether to put workspaces starting with numbers before workspaces that do not start with a number. + typeof: bool ++ + Whether to put workspaces starting with numbers before workspaces that do not start with a number. + +*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. # FORMAT REPLACEMENTS @@ -134,3 +143,4 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge - *#workspaces button.urgent* - *#workspaces button.persistent* - *#workspaces button.current_output* +- *#workspaces button#sway-workspace-${name}* diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 86497367..8d11e517 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -50,6 +50,11 @@ Addressed by *temperature* typeof: array ++ Based on the current temperature (Celsius) and *critical-threshold* if available, the corresponding icon gets selected. The order is *low* to *high*. +*tooltip-format*: ++ + typeof: string ++ + default: {temperatureC}°C ++ + The format for the tooltip + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label. @@ -58,6 +63,14 @@ Addressed by *temperature* 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. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + *on-click*: ++ typeof: string ++ Command to execute when you clicked on the module. diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index cd0e93f6..c664594d 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -16,6 +16,15 @@ Addressed by *tray* 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. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + *spacing*: ++ typeof: integer ++ Defines the spacing between the tray icons. @@ -37,3 +46,6 @@ Addressed by *tray* # STYLE - *#tray* +- *#tray > .passive* +- *#tray > .active* +- *#tray > .needs-attention* diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd new file mode 100644 index 00000000..6645cc24 --- /dev/null +++ b/man/waybar-wlr-taskbar.5.scd @@ -0,0 +1,119 @@ +waybar-wlr-taskbar(5) + +# NAME + +wlroots - Taskbar module + +# DESCRIPTION + +The *taskbar* module displays the currently open applications. This module requires +a compositor that implements the foreign-toplevel-manager interface. + +# CONFIGURATION + +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. + +*format*: ++ + typeof: string ++ + default: {icon} ++ + The format, how information should be displayed. + +*icon-theme*: ++ + typeof: array|string ++ + The names of the icon-themes that should be used to find an icon. The list will be traversed from left to right. If omitted, the system default will be used. + +*icon-size*: ++ + typeof: integer ++ + default: 16 ++ + The size of the icon. + +*markup*: ++ + typeof: bool ++ + default: false ++ + If set to true, pango markup will be accepted in format and tooltip-format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + If set to false no tooltip will be shown. + +*tooltip-format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information in the tooltip should be displayed. + +*active-first*: ++ + typeof: bool ++ + default: false ++ + If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. + +*on-click*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the left mouse button. + +*on-click-middle*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the middle mouse button. + +*on-click-right*: ++ + typeof: string ++ + The action which should be triggered when clicking on the application button with the right mouse button. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*ignore-list*: ++ + typeof: array ++ + List of app_id/titles to be invisible. + +# FORMAT REPLACEMENTS + +*{icon}*: The icon of the application. + +*{title}*: The title of the application. + +*{app_id}*: The app_id (== application name) of the application. + +*{state}*: The state (minimized, maximized, active, fullscreen) of the application. + +*{short_state}*: The state (minimize == m, maximized == M, active == A, fullscreen == F) represented as one character of the application. + +# CLICK ACTIONS + +*activate*: Bring the application into foreground. +*minimize*: Toggle application's minimized state. +*minimize-raise*: Bring the application into foreground or toggle its minimized state. +*maximize*: Toggle application's maximized state. +*fullscreen*: Toggle application's fullscreen state. +*close*: Close the application. + +# EXAMPLES + +``` +"wlr/taskbar": { + "format": "{icon}", + "icon-size": 14, + "icon-theme": "Numix-Circle", + "tooltip-format": "{title}", + "on-click": "activate", + "on-click-middle": "close", + "ignore-list": [ + "Alacritty" + ] +} +``` + +# Style + +- *#taskbar* +- *#taskbar button* +- *#taskbar button.maximized* +- *#taskbar button.minimized* +- *#taskbar button.active* +- *#taskbar button.fullscreen* diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd new file mode 100644 index 00000000..f0df5e94 --- /dev/null +++ b/man/waybar-wlr-workspaces.5.scd @@ -0,0 +1,87 @@ +waybar-wlr-workspaces(5) + +# NAME + +waybar - wlr workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in wayland compositor. + +# CONFIGURATION + +Addressed by *wlr/workspaces* + +*format*: ++ + typeof: string ++ + default: {name} ++ + 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*. + +*sort-by-name*: ++ + typeof: bool ++ + default: true ++ + Should workspaces be sorted by name. + +*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. + +*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 active or urgent workspaces will be shown. + +# FORMAT REPLACEMENTS + +*{name}*: Name of workspace assigned by compositor + +*{icon}*: Icon, as defined in *format-icons*. + +# CLICK ACTIONS + +*activate*: Switch to workspace. +*close*: Close the workspace. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string match is found. +- *focused*: Will be shown, when workspace is focused + +# EXAMPLES + +``` +"wlr/workspaces": { + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "focused": "", + "default": "" + } +} +``` + +# Style + +- *#workspaces* +- *#workspaces button* +- *#workspaces button.active* +- *#workspaces button.urgent* +- *#workspaces button.hidden* diff --git a/man/waybar.5.scd b/man/waybar.5.scd.in similarity index 77% rename from man/waybar.5.scd rename to man/waybar.5.scd.in index 1e8004f2..66d5b2eb 100644 --- a/man/waybar.5.scd +++ b/man/waybar.5.scd.in @@ -14,6 +14,7 @@ Valid locations for this file are: - *~/.config/waybar/config* - *~/waybar/config* - */etc/xdg/waybar/config* +- *@sysconfdir@/xdg/waybar/config* 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. @@ -63,16 +64,37 @@ Also a minimal example configuration can be found on the at the bottom of this m typeof: integer ++ Margins value without units. +*spacing* ++ + typeof: integer ++ + Size of gaps in between of the different modules. + *name* ++ typeof: string ++ Optional name added as a CSS class, for styling multiple waybars. +*exclusive* ++ + typeof: bool ++ + default: *true* unless the layer is set to *overlay* ++ + Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar. + +*passthrough* ++ + typeof: bool ++ + default: *false* unless the layer is set to *overlay* ++ + 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. +*include* ++ + typeof: string|array ++ + Paths to additional configuration files. + 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. + # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). @@ -185,16 +207,23 @@ Valid options for the "rotate" property are: 0, 90, 180 and 270. - *waybar-backlight(5)* - *waybar-battery(5)* +- *waybar-bluetooth(5)* - *waybar-clock(5)* - *waybar-cpu(5)* - *waybar-custom(5)* +- *waybar-disk(5)* - *waybar-idle-inhibitor(5)* +- *waybar-keyboard-state(5)* - *waybar-memory(5)* -- *waybar-mdp(5)* +- *waybar-mpd(5)* - *waybar-network(5)* - *waybar-pulseaudio(5)* +- *waybar-river-tags(5)* +- *waybar-states(5)* - *waybar-sway-mode(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* +- *waybar-wlr-taskbar(5)* +- *waybar-wlr-workspaces(5)* - *waybar-temperature(5)* - *waybar-tray(5)* diff --git a/meson.build b/meson.build index c16d7854..62ac8e36 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,8 @@ project( 'waybar', 'cpp', 'c', - version: '0.9.2', + version: '0.9.8', license: 'MIT', + meson_version: '>= 0.49.0', default_options : [ 'cpp_std=c++17', 'buildtype=release', @@ -36,7 +37,7 @@ else 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() - version = '"@0@ (" __DATE__ ", branch \'@1@\')"'.format(git_commit_hash, git_branch) + version = '"@0@ (branch \'@1@\')"'.format(git_commit_hash, git_branch) add_project_arguments('-DVERSION=@0@'.format(version), language: 'cpp') else add_project_arguments('-DVERSION="@0@"'.format(meson.project_version()), language: 'cpp') @@ -79,7 +80,7 @@ is_openbsd = host_machine.system() == 'openbsd' thread_dep = dependency('threads') fmt = dependency('fmt', version : ['>=5.3.0'], fallback : ['fmt', 'fmt_dep']) -spdlog = dependency('spdlog', version : ['>=1.3.1'], fallback : ['spdlog', 'spdlog_dep']) +spdlog = dependency('spdlog', version : ['>=1.8.5'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=true']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') @@ -93,12 +94,31 @@ libnl = dependency('libnl-3.0', required: get_option('libnl')) libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) libpulse = dependency('libpulse', required: get_option('pulseaudio')) libudev = dependency('libudev', required: get_option('libudev')) +libevdev = dependency('libevdev', required: get_option('libevdev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) +xkbregistry = dependency('xkbregistry') + +libsndio = compiler.find_library('sndio', required: get_option('sndio')) +if libsndio.found() + if not compiler.has_function('sioctl_open', prefix: '#include ', dependencies: libsndio) + if get_option('sndio').enabled() + error('libsndio is too old, required >=1.7.0') + else + warning('libsndio is too old, required >=1.7.0') + libsndio = dependency('', required: false) + endif + endif +endif + gtk_layer_shell = dependency('gtk-layer-shell-0', required: get_option('gtk-layer-shell'), fallback : ['gtk-layer-shell', 'gtk_layer_shell_dep']) systemd = dependency('systemd', required: get_option('systemd')) -tz_dep = dependency('date', default_options : [ 'use_system_tzdb=true' ], fallback: [ 'date', 'tz_dep' ]) +tz_dep = dependency('date', + required: false, + default_options : [ 'use_system_tzdb=true' ], + modules : [ 'date::date', 'date::date-tz' ], + fallback: [ 'date', 'tz_dep' ]) prefix = get_option('prefix') sysconfdir = get_option('sysconfdir') @@ -122,7 +142,6 @@ src_files = files( 'src/factory.cpp', 'src/AModule.cpp', 'src/ALabel.cpp', - 'src/modules/clock.cpp', 'src/modules/custom.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', @@ -130,6 +149,8 @@ src_files = files( 'src/main.cpp', 'src/bar.cpp', 'src/client.cpp', + 'src/config.cpp', + 'src/util/ustring_clen.cpp' ) if is_linux @@ -137,12 +158,10 @@ if is_linux add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') src_files += files( 'src/modules/battery.cpp', - 'src/modules/bluetooth.cpp', 'src/modules/cpu/common.cpp', 'src/modules/cpu/linux.cpp', 'src/modules/memory/common.cpp', 'src/modules/memory/linux.cpp', - 'src/util/rfkill.cpp' ) elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') @@ -155,14 +174,25 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd ) endif -if true # find_program('sway', required : false).found() - add_project_arguments('-DHAVE_SWAY', language: 'cpp') - src_files += [ - 'src/modules/sway/ipc/client.cpp', - 'src/modules/sway/mode.cpp', - 'src/modules/sway/window.cpp', - 'src/modules/sway/workspaces.cpp' - ] +add_project_arguments('-DHAVE_SWAY', language: 'cpp') +src_files += [ + 'src/modules/sway/ipc/client.cpp', + 'src/modules/sway/mode.cpp', + 'src/modules/sway/language.cpp', + 'src/modules/sway/window.cpp', + 'src/modules/sway/workspaces.cpp' +] + +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' +endif + +if true + add_project_arguments('-DHAVE_RIVER', language: 'cpp') + src_files += 'src/modules/river/tags.cpp' endif if libnl.found() and libnlgen.found() @@ -190,15 +220,43 @@ if libudev.found() and (is_linux or libepoll.found()) src_files += 'src/modules/backlight.cpp' endif +if libevdev.found() and (is_linux or libepoll.found()) + add_project_arguments('-DHAVE_LIBEVDEV', language: 'cpp') + src_files += 'src/modules/keyboard_state.cpp' +endif + if libmpdclient.found() add_project_arguments('-DHAVE_LIBMPDCLIENT', language: 'cpp') - src_files += 'src/modules/mpd.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') endif +if libsndio.found() + add_project_arguments('-DHAVE_LIBSNDIO', language: 'cpp') + src_files += 'src/modules/sndio.cpp' +endif + +if get_option('rfkill').enabled() + if is_linux + add_project_arguments('-DWANT_RFKILL', language: 'cpp') + src_files += files( + 'src/modules/bluetooth.cpp', + 'src/util/rfkill.cpp' + ) + endif +endif + +if tz_dep.found() + add_project_arguments('-DHAVE_LIBDATE', language: 'cpp') + src_files += 'src/modules/clock.cpp' +else + src_files += 'src/modules/simpleclock.cpp' +endif + subdir('protocol') executable( @@ -222,8 +280,11 @@ executable( libudev, libepoll, libmpdclient, + libevdev, gtk_layer_shell, - tz_dep + libsndio, + tz_dep, + xkbregistry ], include_directories: [include_directories('include')], install: true, @@ -240,9 +301,20 @@ scdoc = dependency('scdoc', version: '>=1.9.2', native: true, required: get_opti if scdoc.found() scdoc_prog = find_program(scdoc.get_pkgconfig_variable('scdoc'), native: true) sh = find_program('sh', native: true) + + main_manpage = configure_file( + input: 'man/waybar.5.scd.in', + output: 'waybar.5.scd', + configuration: { + 'sysconfdir': join_paths(prefix, sysconfdir) + } + ) + + main_manpage_path = join_paths(meson.build_root(), '@0@'.format(main_manpage)) + mandir = get_option('mandir') man_files = [ - 'waybar.5.scd', + main_manpage_path, 'waybar-backlight.5.scd', 'waybar-battery.5.scd', 'waybar-clock.5.scd', @@ -250,27 +322,37 @@ if scdoc.found() 'waybar-custom.5.scd', 'waybar-disk.5.scd', 'waybar-idle-inhibitor.5.scd', + 'waybar-keyboard-state.5.scd', 'waybar-memory.5.scd', 'waybar-mpd.5.scd', 'waybar-network.5.scd', 'waybar-pulseaudio.5.scd', + 'waybar-river-tags.5.scd', + 'waybar-sway-language.5.scd', 'waybar-sway-mode.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', ] - foreach filename : man_files - topic = filename.split('.')[-3].split('/')[-1] - section = filename.split('.')[-2] + foreach file : man_files + path = '@0@'.format(file) + basename = path.split('/')[-1] + + topic = basename.split('.')[-3] + section = basename.split('.')[-2] output = '@0@.@1@'.format(topic, section) custom_target( output, - input: 'man/@0@'.format(filename), + # drops the 'man' if `path` is an absolute path + input: join_paths('man', path), output: output, command: [ sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc_prog.path(), output) @@ -281,6 +363,15 @@ if scdoc.found() endforeach endif +catch2 = dependency( + 'catch2', + fallback: ['catch2', 'catch2_dep'], + required: get_option('tests'), +) +if catch2.found() + subdir('test') +endif + clangtidy = find_program('clang-tidy', required: false) if clangtidy.found() diff --git a/meson_options.txt b/meson_options.txt index a44ff648..81e44689 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,9 +1,13 @@ option('libcxx', type : 'boolean', value : false, description : 'Build with Clang\'s libc++ instead of libstdc++ on Linux.') option('libnl', type: 'feature', value: 'auto', description: 'Enable libnl support for network related features') option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev support for udev related features') +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('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('tests', type: 'feature', value: 'auto', description: 'Enable tests') diff --git a/protocol/ext-workspace-unstable-v1.xml b/protocol/ext-workspace-unstable-v1.xml new file mode 100644 index 00000000..24410b62 --- /dev/null +++ b/protocol/ext-workspace-unstable-v1.xml @@ -0,0 +1,306 @@ + + + + Copyright © 2019 Christopher Billington + Copyright © 2020 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Workspaces, also called virtual desktops, are groups of surfaces. A + compositor with a concept of workspaces may only show some such groups of + surfaces (those of 'active' workspaces) at a time. 'Activating' a + workspace is a request for the compositor to display that workspace's + surfaces as normal, whereas the compositor may hide or otherwise + de-emphasise surfaces that are associated only with 'inactive' workspaces. + Workspaces are grouped by which sets of outputs they correspond to, and + may contain surfaces only from those outputs. In this way, it is possible + for each output to have its own set of workspaces, or for all outputs (or + any other arbitrary grouping) to share workspaces. Compositors may + optionally conceptually arrange each group of workspaces in an + N-dimensional grid. + + The purpose of this protocol is to enable the creation of taskbars and + docks by providing them with a list of workspaces and their properties, + and allowing them to activate and deactivate workspaces. + + After a client binds the zext_workspace_manager_v1, each workspace will be + sent via the workspace event. + + + + + This event is emitted whenever a new workspace group has been created. + + All initial details of the workspace group (workspaces, outputs) will be + sent immediately after this event via the corresponding events in + zext_workspace_group_handle_v1. + + + + + + + The client must send this request after it has finished sending other + requests. The compositor must process a series of requests preceding a + commit request atomically. + + This allows changes to the workspace properties to be seen as atomic, + even if they happen via multiple events, and even if they involve + multiple zext_workspace_handle_v1 objects, for example, deactivating one + workspace and activating another. + + + + + + This event is sent after all changes in all workspace groups have been + sent. + + This allows changes to one or more zext_workspace_group_handle_v1 + properties to be seen as atomic, even if they happen via multiple + events. In particular, an output moving from one workspace group to + another sends an output_enter event and an output_leave event to the two + zext_workspace_group_handle_v1 objects in question. The compositor sends + the done event only after updating the output information in both + workspace groups. + + + + + + This event indicates that the compositor is done sending events to the + zext_workspace_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + Indicates the client no longer wishes to receive events for new + workspace groups. However the compositor may emit further workspace + events, until the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + + A zext_workspace_group_handle_v1 object represents a a workspace group + that is assigned a set of outputs and contains a number of workspaces. + + The set of outputs assigned to the workspace group is conveyed to the client via + output_enter and output_leave events, and its workspaces are conveyed with + workspace events. + + For example, a compositor which has a set of workspaces for each output may + advertise a workspace group (and its workspaces) per output, whereas a compositor + where a workspace spans all outputs may advertise a single workspace group for all + outputs. + + + + + This event is emitted whenever an output is assigned to the workspace + group. + + + + + + + This event is emitted whenever an output is removed from the workspace + group. + + + + + + + This event is emitted whenever a new workspace has been created. + + All initial details of the workspace (name, coordinates, state) will + be sent immediately after this event via the corresponding events in + zext_workspace_handle_v1. + + + + + + + This event means the zext_workspace_group_handle_v1 has been destroyed. + It is guaranteed there won't be any more events for this + zext_workspace_group_handle_v1. The zext_workspace_group_handle_v1 becomes + inert so any requests will be ignored except the destroy request. + + The compositor must remove all workspaces belonging to a workspace group + before removing the workspace group. + + + + + + Request that the compositor create a new workspace with the given name. + + There is no guarantee that the compositor will create a new workspace, + or that the created workspace will have the provided name. + + + + + + + Destroys the zext_workspace_handle_v1 object. + + This request should be called either when the client does not want to + use the workspace object any more or after the remove event to finalize + the destruction of the object. + + + + + + + A zext_workspace_handle_v1 object represents a a workspace that handles a + group of surfaces. + + Each workspace has a name, conveyed to the client with the name event; a + list of states, conveyed to the client with the state event; and + optionally a set of coordinates, conveyed to the client with the + coordinates event. The client may request that the compositor activate or + deactivate the workspace. + + Each workspace can belong to only a single workspace group. + Depepending on the compositor policy, there might be workspaces with + the same name in different workspace groups, but these workspaces are still + separate (e.g. one of them might be active while the other is not). + + + + + This event is emitted immediately after the zext_workspace_handle_v1 is + created and whenever the name of the workspace changes. + + + + + + + This event is used to organize workspaces into an N-dimensional grid + within a workspace group, and if supported, is emitted immediately after + the zext_workspace_handle_v1 is created and whenever the coordinates of + the workspace change. Compositors may not send this event if they do not + conceptually arrange workspaces in this way. If compositors simply + number workspaces, without any geometric interpretation, they may send + 1D coordinates, which clients should not interpret as implying any + geometry. Sending an empty array means that the compositor no longer + orders the workspace geometrically. + + Coordinates have an arbitrary number of dimensions N with an uint32 + position along each dimension. By convention if N > 1, the first + dimension is X, the second Y, the third Z, and so on. The compositor may + chose to utilize these events for a more novel workspace layout + convention, however. No guarantee is made about the grid being filled or + bounded; there may be a workspace at coordinate 1 and another at + coordinate 1000 and none in between. Within a workspace group, however, + workspaces must have unique coordinates of equal dimensionality. + + + + + + + This event is emitted immediately after the zext_workspace_handle_v1 is + created and each time the workspace state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + The different states that a workspace can have. + + + + + + + The workspace is not visible in its workspace group, and clients + attempting to visualize the compositor workspace state should not + display such workspaces. + + + + + + + This event means the zext_workspace_handle_v1 has been destroyed. It is + guaranteed there won't be any more events for this + zext_workspace_handle_v1. The zext_workspace_handle_v1 becomes inert so + any requests will be ignored except the destroy request. + + + + + + Destroys the zext_workspace_handle_v1 object. + + This request should be called either when the client does not want to + use the workspace object any more or after the remove event to finalize + the destruction of the object. + + + + + + Request that this workspace be activated. + + There is no guarantee the workspace will be actually activated, and + behaviour may be compositor-dependent. For example, activating a + workspace may or may not deactivate all other workspaces in the same + group. + + + + + + Request that this workspace be deactivated. + + There is no guarantee the workspace will be actually deactivated. + + + + + + Request that this workspace be removed. + + There is no guarantee the workspace will be actually removed. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index 0699a9d9..6e82d63d 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -26,6 +26,10 @@ client_protocols = [ [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'], ['wlr-layer-shell-unstable-v1.xml'], + ['wlr-foreign-toplevel-management-unstable-v1.xml'], + ['ext-workspace-unstable-v1.xml'], + ['river-status-unstable-v1.xml'], + ['river-control-unstable-v1.xml'], ] client_protos_src = [] diff --git a/protocol/river-control-unstable-v1.xml b/protocol/river-control-unstable-v1.xml new file mode 100644 index 00000000..b8faa45c --- /dev/null +++ b/protocol/river-control-unstable-v1.xml @@ -0,0 +1,85 @@ + + + + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, 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. + + + + + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and + executed with a run_command request. The first argument is the command + to be run. + + A complete list of commands should be made available in the man page of + the compositor. + + + + + This request indicates that the client will not use the + river_control object any more. Objects that have been created + through this instance are not affected. + + + + + + Arguments are stored by the server in the order they were sent until + the run_command request is made. + + + + + + + Execute the command built up using the add_argument request for the + given seat. + + + + + + + + + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed + by the compositor after one of the events is sent. + + + + + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + + + + + + + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + + + + + diff --git a/protocol/river-status-unstable-v1.xml b/protocol/river-status-unstable-v1.xml new file mode 100644 index 00000000..13affaa7 --- /dev/null +++ b/protocol/river-status-unstable-v1.xml @@ -0,0 +1,125 @@ + + + + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, 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. + + + + + A global factory for objects that receive status information specific + to river. It could be used to implement, for example, a status bar. + + + + + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + + + + + + This creates a new river_output_status object for the given wl_output. + + + + + + + + This creates a new river_seat_status object for the given wl_seat. + + + + + + + + + This interface allows clients to receive information about the current + windowing state of an output. + + + + + This request indicates that the client will not use the + river_output_status object any more. + + + + + + Sent once binding the interface and again whenever the tag focus of + the output changes. + + + + + + + Sent once on binding the interface and again whenever the tag state + of the output changes. + + + + + + + Sent once on binding the interface and again whenever the set of + tags with at least one urgent view changes. + + + + + + + + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. + + + + + This request indicates that the client will not use the + river_seat_status object any more. + + + + + + Sent on binding the interface and again whenever an output gains focus. + + + + + + + Sent whenever an output loses focus. + + + + + + + Sent once on binding the interface and again whenever the focused + view or a property thereof changes. The title may be an empty string + if no view is focused or the focused view did not set a title. + + + + + diff --git a/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..10813371 --- /dev/null +++ b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/protocol/wlr-layer-shell-unstable-v1.xml b/protocol/wlr-layer-shell-unstable-v1.xml index fb4f6b23..f9a4fe05 100644 --- a/protocol/wlr-layer-shell-unstable-v1.xml +++ b/protocol/wlr-layer-shell-unstable-v1.xml @@ -25,7 +25,7 @@ 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 @@ -82,17 +82,27 @@ + + + + + + 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 (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. + 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. @@ -115,7 +125,7 @@ Requests that the compositor anchor the surface to the specified edges - and corners. If two orthoginal edges are specified (e.g. 'top' and + 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. @@ -127,20 +137,25 @@ - Requests that the compositor avoids occluding an area of the surface - with other surfaces. The compositor's use of this information is + 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 an - edge, rather than a corner. The zone is the number of surface-local - coordinates from the edge that are considered exclusive. + 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 excluzive zone. If set to -1, the surface - indicates that it would not like to be moved to accomodate for other + 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. @@ -281,5 +296,16 @@ + + + + + + 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 index 832f76c8..063393b0 100644 --- a/resources/config +++ b/resources/config @@ -3,10 +3,11 @@ // "position": "bottom", // Waybar position (top|bottom|left|right) "height": 30, // Waybar height (to be removed for auto height) // "width": 1280, // Waybar width + "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules "modules-left": ["sway/workspaces", "sway/mode", "custom/media"], "modules-center": ["sway/window"], - "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "battery", "battery#bat2", "clock", "tray"], + "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "keyboard-state", "sway/language", "battery", "battery#bat2", "clock", "tray"], // Modules configuration // "sway/workspaces": { // "disable-scroll": true, @@ -23,11 +24,20 @@ // "default": "" // } // }, + "keyboard-state": { + "numlock": true, + "capslock": true, + "format": "{name} {icon}", + "format-icons": { + "locked": "", + "unlocked": "" + } + }, "sway/mode": { "format": "{}" }, "mpd": { - "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ", + "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ⸨{songPosition}|{queueLength}⸩ {volume}% ", "format-disconnected": "Disconnected ", "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", "unknown-tag": "N/A", @@ -108,7 +118,8 @@ "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", - "format-ethernet": "{ifname}: {ipaddr}/{cidr} ", + "format-ethernet": "{ipaddr}/{cidr} ", + "tooltip-format": "{ifname} via {gwaddr} ", "format-linked": "{ifname} (No IP) ", "format-disconnected": "Disconnected ⚠", "format-alt": "{ifname}: {ipaddr}/{cidr}" @@ -145,3 +156,4 @@ // "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name } } + diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index cf3df4b6..fa9aa58b 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -79,7 +79,7 @@ def signal_handler(sig, frame): def parse_arguments(): parser = argparse.ArgumentParser() - # Increase verbosity with every occurence of -v + # Increase verbosity with every occurrence of -v parser.add_argument('-v', '--verbose', action='count', default=0) # Define for which player we're listening diff --git a/resources/style.css b/resources/style.css index e21ae00e..0235942b 100644 --- a/resources/style.css +++ b/resources/style.css @@ -41,19 +41,19 @@ window#waybar.chromium { padding: 0 5px; background-color: transparent; color: #ffffff; - border-bottom: 3px solid transparent; + /* Use box-shadow instead of border so the text isn't offset */ + box-shadow: inset 0 -3px transparent; } /* https://github.com/Alexays/Waybar/wiki/FAQ#the-workspace-buttons-have-a-strange-hover-effect */ #workspaces button:hover { background: rgba(0, 0, 0, 0.2); - box-shadow: inherit; - border-bottom: 3px solid #ffffff; + box-shadow: inset 0 -3px #ffffff; } #workspaces button.focused { background-color: #64727D; - border-bottom: 3px solid #ffffff; + box-shadow: inset 0 -3px #ffffff; } #workspaces button.urgent { @@ -69,6 +69,7 @@ window#waybar.chromium { #battery, #cpu, #memory, +#disk, #temperature, #backlight, #network, @@ -79,10 +80,24 @@ window#waybar.chromium { #idle_inhibitor, #mpd { padding: 0 10px; - margin: 0 4px; color: #ffffff; } +#window, +#workspaces { + margin: 0 4px; +} + +/* If workspaces is the leftmost module, omit left margin */ +.modules-left > widget:first-child > #workspaces { + margin-left: 0; +} + +/* If workspaces is the rightmost module, omit right margin */ +.modules-right > widget:last-child > #workspaces { + margin-right: 0; +} + #clock { background-color: #64727D; } @@ -92,7 +107,7 @@ window#waybar.chromium { color: #000000; } -#battery.charging { +#battery.charging, #battery.plugged { color: #ffffff; background-color: #26A65B; } @@ -127,6 +142,10 @@ label:focus { background-color: #9b59b6; } +#disk { + background-color: #964B00; +} + #backlight { background-color: #90b1b1; } @@ -175,6 +194,15 @@ label:focus { background-color: #2980b9; } +#tray > .passive { + -gtk-icon-effect: dim; +} + +#tray > .needs-attention { + -gtk-icon-effect: highlight; + background-color: #eb4d4b; +} + #idle_inhibitor { background-color: #2d3436; } @@ -200,3 +228,27 @@ label:focus { #mpd.paused { background-color: #51a37a; } + +#language { + background: #00b093; + color: #740864; + padding: 0 5px; + margin: 0 5px; + min-width: 16px; +} + +#keyboard-state { + background: #97e1ad; + color: #000000; + padding: 0 0px; + margin: 0 5px; + min-width: 16px; +} + +#keyboard-state > label { + padding: 0 5px; +} + +#keyboard-state > label.locked { + background: rgba(0, 0, 0, 0.2); +} diff --git a/resources/waybar.service.in b/resources/waybar.service.in index 03262a33..81ac6779 100644 --- a/resources/waybar.service.in +++ b/resources/waybar.service.in @@ -1,13 +1,14 @@ [Unit] Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. Documentation=https://github.com/Alexays/Waybar/wiki/ -PartOf=wayland-session.target -After=wayland-session.target +PartOf=graphical-session.target +After=graphical-session.target +Requisite=graphical-session.target [Service] -Type=dbus -BusName=fr.arouillard.waybar ExecStart=@prefix@/bin/waybar +ExecReload=kill -SIGUSR2 $MAINPID +Restart=on-failure [Install] -WantedBy=wayland-session.target +WantedBy=graphical-session.target diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 68313e09..b9b3d1d2 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -5,8 +5,9 @@ namespace waybar { ALabel::ALabel(const Json::Value& config, const std::string& name, const std::string& id, - const std::string& format, uint16_t interval, bool ellipsize) - : AModule(config, name, id, config["format-alt"].isString()), + 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), format_(config_["format"].isString() ? config_["format"].asString() : format), interval_(config_["interval"] == "once" ? std::chrono::seconds(100000000) @@ -19,15 +20,36 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } event_box_.add(label_); if (config_["max-length"].isUInt()) { - label_.set_max_width_chars(config_["max-length"].asUInt()); + label_.set_max_width_chars(config_["max-length"].asInt()); label_.set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END); + label_.set_single_line_mode(true); } else if (ellipsize && label_.get_max_width_chars() == -1) { label_.set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END); + label_.set_single_line_mode(true); } - if (config_["rotate"].isUInt()) { - label_.set_angle(config["rotate"].asUInt()); + if (config_["min-length"].isUInt()) { + label_.set_width_chars(config_["min-length"].asUInt()); } + + uint rotate = 0; + + if (config_["rotate"].isUInt()) { + rotate = config["rotate"].asUInt(); + label_.set_angle(rotate); + } + + if (config_["align"].isDouble()) { + auto align = config_["align"].asFloat(); + if (rotate == 90 || rotate == 270) { + label_.set_yalign(align); + } else { + label_.set_xalign(align); + } + + } + + } auto ALabel::update() -> void { @@ -45,8 +67,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ } if (format_icons.isArray()) { auto size = format_icons.size(); - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); - format_icons = format_icons[idx]; + if (size) { + auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + format_icons = format_icons[idx]; + } } if (format_icons.isString()) { return format_icons.asString(); @@ -54,22 +78,24 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ return ""; } -std::string ALabel::getIcon(uint16_t percentage, std::vector& alts, uint16_t max) { +std::string ALabel::getIcon(uint16_t percentage, const std::vector& alts, uint16_t max) { auto format_icons = config_["format-icons"]; if (format_icons.isObject()) { + std::string _alt = "default"; for (const auto& alt : alts) { if (!alt.empty() && (format_icons[alt].isString() || format_icons[alt].isArray())) { - format_icons = format_icons[alt]; + _alt = alt; break; - } else { - format_icons = format_icons["default"]; } } + format_icons = format_icons[_alt]; } if (format_icons.isArray()) { auto size = format_icons.size(); - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); - format_icons = format_icons[idx]; + if (size) { + auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + format_icons = format_icons[idx]; + } } if (format_icons.isString()) { return format_icons.asString(); diff --git a/src/AModule.cpp b/src/AModule.cpp index 3066bfc1..7da942e0 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -6,7 +6,7 @@ namespace waybar { AModule::AModule(const Json::Value& config, const std::string& name, const std::string& id, bool enable_click, bool enable_scroll) - : config_(std::move(config)) { + : name_(std::move(name)), config_(std::move(config)) { // configure events' user commands if (config_["on-click"].isString() || config_["on-click-middle"].isString() || config_["on-click-backward"].isString() || config_["on-click-forward"].isString() || @@ -23,11 +23,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: AModule::~AModule() { for (const auto& pid : pid_) { if (pid != -1) { - kill(-pid, 9); + killpg(pid, SIGTERM); } } } + auto AModule::update() -> void { // Run user-provided update handler if configured if (config_["on-update"].isString()) { @@ -43,9 +44,9 @@ bool AModule::handleToggle(GdkEventButton* const& e) { format = config_["on-click-middle"].asString(); } else if (config_["on-click-right"].isString() && e->button == 3) { format = config_["on-click-right"].asString(); - } else if (config_["on-click-forward"].isString() && e->button == 8) { + } else if (config_["on-click-backward"].isString() && e->button == 8) { format = config_["on-click-backward"].asString(); - } else if (config_["on-click-backward"].isString() && e->button == 9) { + } else if (config_["on-click-forward"].isString() && e->button == 9) { format = config_["on-click-forward"].asString(); } if (!format.empty()) { diff --git a/src/bar.cpp b/src/bar.cpp index 431a5642..a8b230e1 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -2,18 +2,396 @@ #include #endif +#include + +#include + #include "bar.hpp" #include "client.hpp" #include "factory.hpp" -#include +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +namespace waybar { +static constexpr const char* MIN_HEIGHT_MSG = + "Requested height: {} is less than the minimum height: {} required by the modules"; + +static constexpr const char* MIN_WIDTH_MSG = + "Requested width: {} is less than the minimum width: {} required by the modules"; + +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"; + +#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); + } + } + + void setSize(uint32_t width, uint32_t height) override { + width_ = width; + height_ = height; + window_.set_size_request(width_, height_); + }; + + private: + Gtk::Window& window_; + std::string output_name_; + uint32_t width_; + uint32_t height_; + bool passthrough_ = false; + bool vertical_ = false; + + void onMap(GdkEventAny* ev) { setPassThrough(passthrough_); } + + void onConfigure(GdkEventConfigure* ev) { + /* + * GTK wants new size for the window. + * Actual resizing and management of the exclusve zone is handled within the gtk-layer-shell + * code. This event handler only updates stored size of the window and prints some warnings. + * + * Note: forced resizing to a window smaller than required by GTK would not work with + * gtk-layer-shell. + */ + if (vertical_) { + if (width_ > 1 && ev->width > static_cast(width_)) { + spdlog::warn(MIN_WIDTH_MSG, width_, ev->width); + } + } else { + if (height_ > 1 && ev->height > static_cast(height_)) { + spdlog::warn(MIN_HEIGHT_MSG, height_, ev->height); + } + } + width_ = ev->width; + height_ = ev->height; + spdlog::info(BAR_SIZE_MSG, width_, height_, output_name_); + } +}; +#endif + +struct RawSurfaceImpl : public BarSurface, public sigc::trackable { + RawSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { + output_ = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); + output_name_ = output.name; + + window.signal_realize().connect_notify(sigc::mem_fun(*this, &RawSurfaceImpl::onRealize)); + window.signal_map_event().connect_notify(sigc::mem_fun(*this, &RawSurfaceImpl::onMap)); + window.signal_configure_event().connect_notify( + sigc::mem_fun(*this, &RawSurfaceImpl::onConfigure)); + + if (window.get_realized()) { + onRealize(); + } + } + + void setExclusiveZone(bool enable) override { + exclusive_zone_ = enable; + if (layer_surface_) { + auto zone = 0; + if (enable) { + // exclusive zone already includes margin for anchored edge, + // only opposite margin should be added + if ((anchor_ & VERTICAL_ANCHOR) == VERTICAL_ANCHOR) { + zone += width_; + zone += (anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) ? margins_.right : margins_.left; + } else { + zone += height_; + zone += (anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) ? margins_.bottom : margins_.top; + } + } + spdlog::debug("Set exclusive zone {} for output {}", zone, output_name_); + zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_.get(), zone); + } + } + + void setLayer(bar_layer layer) override { + layer_ = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + if (layer == bar_layer::TOP) { + layer_ = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + } else if (layer == bar_layer::OVERLAY) { + layer_ = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + } + // updating already mapped window + if (layer_surface_) { + if (zwlr_layer_surface_v1_get_version(layer_surface_.get()) >= + ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION) { + zwlr_layer_surface_v1_set_layer(layer_surface_.get(), layer_); + } else { + spdlog::warn("Unable to change layer: layer-shell implementation is too old"); + } + } + } + + void setMargins(const struct bar_margins& margins) override { + margins_ = margins; + // updating already mapped window + if (layer_surface_) { + zwlr_layer_surface_v1_set_margin( + layer_surface_.get(), margins_.top, margins_.right, margins_.bottom, margins_.left); + } + } + + void setPassThrough(bool enable) override { + passthrough_ = enable; + /* GTK overwrites any region changes applied directly to the wl_surface, + * thus the same GTK region API as in the GLS impl has to be used. */ + auto gdk_window = window_.get_window(); + if (gdk_window) { + Cairo::RefPtr region; + if (enable) { + region = Cairo::Region::create(); + } + gdk_window->input_shape_combine_region(region, 0, 0); + } + } + + void setPosition(const std::string_view& position) override { + anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + if (position == "bottom") { + anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + } else if (position == "left") { + anchor_ = VERTICAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; + } else if (position == "right") { + anchor_ = VERTICAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + } + + // updating already mapped window + if (layer_surface_) { + zwlr_layer_surface_v1_set_anchor(layer_surface_.get(), anchor_); + } + } + + void setSize(uint32_t width, uint32_t height) override { + configured_width_ = width_ = width; + configured_height_ = height_ = height; + // layer_shell.configure handler should update exclusive zone if size changes + window_.set_size_request(width, height); + }; + + void commit() override { + if (surface_) { + wl_surface_commit(surface_); + } + } + + private: + constexpr static uint8_t VERTICAL_ANCHOR = + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + constexpr static uint8_t HORIZONTAL_ANCHOR = + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + + template + using deleter_fn = std::integral_constant; + using layer_surface_ptr = + std::unique_ptr>; + + Gtk::Window& window_; + std::string output_name_; + uint32_t configured_width_ = 0; + uint32_t configured_height_ = 0; + uint32_t width_ = 0; + uint32_t height_ = 0; + uint8_t anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + bool exclusive_zone_ = true; + bool passthrough_ = false; + struct bar_margins margins_; + + zwlr_layer_shell_v1_layer layer_ = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + struct wl_output* output_ = nullptr; // owned by GTK + struct wl_surface* surface_ = nullptr; // owned by GTK + layer_surface_ptr layer_surface_; + + void onRealize() { + auto gdk_window = window_.get_window()->gobj(); + gdk_wayland_window_set_use_custom_surface(gdk_window); + } + + void onMap(GdkEventAny* ev) { + static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = onSurfaceConfigure, + .closed = onSurfaceClosed, + }; + auto client = Client::inst(); + auto gdk_window = window_.get_window()->gobj(); + surface_ = gdk_wayland_window_get_wl_surface(gdk_window); + + layer_surface_.reset(zwlr_layer_shell_v1_get_layer_surface( + client->layer_shell, surface_, output_, layer_, "waybar")); + + zwlr_layer_surface_v1_add_listener(layer_surface_.get(), &layer_surface_listener, this); + zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface_.get(), false); + zwlr_layer_surface_v1_set_anchor(layer_surface_.get(), anchor_); + zwlr_layer_surface_v1_set_margin( + layer_surface_.get(), margins_.top, margins_.right, margins_.bottom, margins_.left); + + setSurfaceSize(width_, height_); + setExclusiveZone(exclusive_zone_); + setPassThrough(passthrough_); + + commit(); + wl_display_roundtrip(client->wl_display); + } + + void onConfigure(GdkEventConfigure* ev) { + /* + * GTK wants new size for the window. + * + * Prefer configured size if it's non-default. + * If the size is not set and the window is smaller than requested by GTK, request resize from + * layer surface. + */ + auto tmp_height = height_; + auto tmp_width = width_; + if (ev->height > static_cast(height_)) { + // Default minimal value + if (height_ > 1) { + spdlog::warn(MIN_HEIGHT_MSG, height_, ev->height); + } + if (configured_height_ > 1) { + spdlog::info(SIZE_DEFINED, "Height"); + } else { + tmp_height = ev->height; + } + } + if (ev->width > static_cast(width_)) { + // Default minimal value + if (width_ > 1) { + spdlog::warn(MIN_WIDTH_MSG, width_, ev->width); + } + if (configured_width_ > 1) { + spdlog::info(SIZE_DEFINED, "Width"); + } else { + tmp_width = ev->width; + } + } + if (tmp_width != width_ || tmp_height != height_) { + setSurfaceSize(tmp_width, tmp_height); + commit(); + } + } + + void setSurfaceSize(uint32_t width, uint32_t height) { + /* If the client is anchored to two opposite edges, layer_surface.configure will return + * size without margins for the axis. + * layer_surface.set_size, however, expects size with margins for the anchored axis. + * This is not specified by wlr-layer-shell and based on actual behavior of sway. + * + * If the size for unanchored axis is not set (0), change request to 1 to avoid automatic + * assignment by the compositor. + */ + if ((anchor_ & VERTICAL_ANCHOR) == VERTICAL_ANCHOR) { + width = width > 0 ? width : 1; + if (height > 1) { + height += margins_.top + margins_.bottom; + } + } else { + height = height > 0 ? height : 1; + if (width > 1) { + width += margins_.right + margins_.left; + } + } + spdlog::debug("Set surface size {}x{} for output {}", width, height, output_name_); + zwlr_layer_surface_v1_set_size(layer_surface_.get(), width, height); + } + + static void onSurfaceConfigure(void* data, struct zwlr_layer_surface_v1* surface, uint32_t serial, + uint32_t width, uint32_t height) { + auto o = static_cast(data); + if (width != o->width_ || height != o->height_) { + o->width_ = width; + o->height_ = height; + o->window_.set_size_request(o->width_, o->height_); + o->window_.resize(o->width_, o->height_); + o->setExclusiveZone(o->exclusive_zone_); + spdlog::info(BAR_SIZE_MSG, + o->width_ == 1 ? "auto" : std::to_string(o->width_), + o->height_ == 1 ? "auto" : std::to_string(o->height_), + o->output_name_); + o->commit(); + } + zwlr_layer_surface_v1_ack_configure(surface, serial); + } + + static void onSurfaceClosed(void* data, struct zwlr_layer_surface_v1* /* surface */) { + auto o = static_cast(data); + o->layer_surface_.reset(); + } +}; + +}; // namespace waybar waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) : output(w_output), config(w_config), window{Gtk::WindowType::WINDOW_TOPLEVEL}, - surface(nullptr), - layer_surface_(nullptr), - anchor_(ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP), + layer_{bar_layer::BOTTOM}, left_(Gtk::ORIENTATION_HORIZONTAL, 0), center_(Gtk::ORIENTATION_HORIZONTAL, 0), right_(Gtk::ORIENTATION_HORIZONTAL, 0), @@ -25,26 +403,30 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.get_style_context()->add_class(config["name"].asString()); window.get_style_context()->add_class(config["position"].asString()); - if (config["position"] == "right" || config["position"] == "left") { - height_ = 0; - width_ = 1; + if (config["layer"] == "top") { + layer_ = bar_layer::TOP; + } else if (config["layer"] == "overlay") { + layer_ = bar_layer::OVERLAY; } - height_ = config["height"].isUInt() ? config["height"].asUInt() : height_; - width_ = config["width"].isUInt() ? config["width"].asUInt() : width_; - if (config["position"] == "bottom") { - anchor_ = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; - } else if (config["position"] == "left") { - anchor_ = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; - } else if (config["position"] == "right") { - anchor_ = ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + if (config["exclusive"].isBool()) { + exclusive = config["exclusive"].asBool(); + } else if (layer_ == bar_layer::OVERLAY) { + // swaybar defaults: overlay mode does not reserve an exclusive zone + exclusive = false; } - if (anchor_ == ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM || - anchor_ == ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) { - anchor_ |= ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; - } else if (anchor_ == ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT || - anchor_ == ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT) { - anchor_ |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + + bool passthrough = false; + if (config["passthrough"].isBool()) { + passthrough = config["passthrough"].asBool(); + } else if (layer_ == bar_layer::OVERLAY) { + // swaybar defaults: overlay mode does not accept pointer events. + passthrough = true; + } + + auto position = config["position"].asString(); + + 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); @@ -52,6 +434,22 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) vertical = true; } + left_.get_style_context()->add_class("modules-left"); + center_.get_style_context()->add_class("modules-center"); + right_.get_style_context()->add_class("modules-right"); + + if (config["spacing"].isInt()) { + int spacing = config["spacing"].asInt(); + left_.set_spacing(spacing); + center_.set_spacing(spacing); + 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_; + if (config["margin-top"].isInt() || config["margin-right"].isInt() || config["margin-bottom"].isInt() || config["margin-left"].isInt()) { margins_ = { @@ -98,170 +496,63 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } #ifdef HAVE_GTK_LAYER_SHELL - use_gls_ = config["gtk-layer-shell"].isBool() ? config["gtk-layer-shell"].asBool() : true; - if (use_gls_) { - initGtkLayerShell(); - } -#endif - - window.signal_realize().connect_notify(sigc::mem_fun(*this, &Bar::onRealize)); - window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); - window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Bar::onConfigure)); - window.set_size_request(width_, height_); - setupWidgets(); - - if (window.get_realized()) { - onRealize(); - } - window.show_all(); -} - -void waybar::Bar::onConfigure(GdkEventConfigure* ev) { - 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 (config["height"].isUInt()) { - 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 (config["width"].isUInt()) { - spdlog::info(SIZE_DEFINED, "Width"); - } else { - tmp_width = ev->width; - } - } - if (use_gls_) { - width_ = tmp_width; - height_ = tmp_height; - spdlog::debug("Set surface size {}x{} for output {}", width_, height_, output->name); - setExclusiveZone(tmp_width, tmp_height); - } else if (tmp_width != width_ || tmp_height != height_) { - setSurfaceSize(tmp_width, tmp_height); - } -} - -#ifdef HAVE_GTK_LAYER_SHELL -void waybar::Bar::initGtkLayerShell() { - auto gtk_window = window.gobj(); - // this has to be executed before GtkWindow.realize - gtk_layer_init_for_window(gtk_window); - gtk_layer_set_keyboard_interactivity(gtk_window, FALSE); - auto layer = config["layer"] == "top" ? GTK_LAYER_SHELL_LAYER_TOP : GTK_LAYER_SHELL_LAYER_BOTTOM; - gtk_layer_set_layer(gtk_window, layer); - gtk_layer_set_monitor(gtk_window, output->monitor->gobj()); - gtk_layer_set_namespace(gtk_window, "waybar"); - - gtk_layer_set_anchor( - gtk_window, GTK_LAYER_SHELL_EDGE_LEFT, anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); - gtk_layer_set_anchor( - gtk_window, GTK_LAYER_SHELL_EDGE_RIGHT, anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); - gtk_layer_set_anchor( - gtk_window, GTK_LAYER_SHELL_EDGE_TOP, anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP); - gtk_layer_set_anchor( - gtk_window, GTK_LAYER_SHELL_EDGE_BOTTOM, anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM); - - 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); - - if (width_ > 1 && height_ > 1) { - /* configure events are not emitted if the bar is using initial size */ - setExclusiveZone(width_, height_); - } -} -#endif - -void waybar::Bar::onRealize() { - auto gdk_window = window.get_window()->gobj(); - gdk_wayland_window_set_use_custom_surface(gdk_window); -} - -void waybar::Bar::onMap(GdkEventAny* ev) { - auto gdk_window = window.get_window()->gobj(); - surface = gdk_wayland_window_get_wl_surface(gdk_window); - - if (use_gls_) { - return; - } - - auto client = waybar::Client::inst(); - // owned by output->monitor; no need to destroy - auto wl_output = gdk_wayland_monitor_get_wl_output(output->monitor->gobj()); - auto layer = - config["layer"] == "top" ? ZWLR_LAYER_SHELL_V1_LAYER_TOP : ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; - layer_surface_ = zwlr_layer_shell_v1_get_layer_surface( - client->layer_shell, surface, wl_output, layer, "waybar"); - - zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface_, false); - zwlr_layer_surface_v1_set_anchor(layer_surface_, anchor_); - zwlr_layer_surface_v1_set_margin( - layer_surface_, margins_.top, margins_.right, margins_.bottom, margins_.left); - setSurfaceSize(width_, height_); - setExclusiveZone(width_, height_); - - static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { - .configure = layerSurfaceHandleConfigure, - .closed = layerSurfaceHandleClosed, - }; - zwlr_layer_surface_v1_add_listener(layer_surface_, &layer_surface_listener, this); - - wl_surface_commit(surface); - wl_display_roundtrip(client->wl_display); -} - -void waybar::Bar::setExclusiveZone(uint32_t width, uint32_t height) { - auto zone = 0; - if (visible) { - // exclusive zone already includes margin for anchored edge, - // only opposite margin should be added - if (vertical) { - 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); - -#ifdef HAVE_GTK_LAYER_SHELL - if (use_gls_) { - gtk_layer_set_exclusive_zone(window.gobj(), zone); + 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 { - zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_, zone); + surface_impl_ = std::make_unique(window, *output); + } + + surface_impl_->setLayer(layer_); + surface_impl_->setExclusiveZone(exclusive); + surface_impl_->setMargins(margins_); + surface_impl_->setPassThrough(passthrough); + surface_impl_->setPosition(position); + surface_impl_->setSize(width, height); + + window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); + + setupWidgets(); + window.show_all(); + + if (spdlog::should_log(spdlog::level::debug)) { + // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. + char* gtk_tree = gtk_style_context_to_string( + window.get_style_context()->gobj(), + (GtkStyleContextPrintFlags)(GTK_STYLE_CONTEXT_PRINT_RECURSE | + GTK_STYLE_CONTEXT_PRINT_SHOW_STYLE)); + spdlog::debug("GTK widget tree:\n{}", gtk_tree); + g_free(gtk_tree); } } -void waybar::Bar::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. +void waybar::Bar::onMap(GdkEventAny*) { + /* + * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). */ - if (vertical && height > 1) { - height += margins_.top + margins_.bottom; - } - if (!vertical && 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_, width, height); + auto gdk_window = window.get_window()->gobj(); + surface = gdk_wayland_window_get_wl_surface(gdk_window); } +void waybar::Bar::setVisible(bool value) { + visible = value; + if (!visible) { + window.get_style_context()->add_class("hidden"); + window.set_opacity(0); + surface_impl_->setLayer(bar_layer::BOTTOM); + } else { + window.get_style_context()->remove_class("hidden"); + window.set_opacity(1); + surface_impl_->setLayer(layer_); + } + surface_impl_->setExclusiveZone(exclusive && visible); + surface_impl_->commit(); +} + +void waybar::Bar::toggle() { setVisible(!visible); } + // Converting string to button code rn as to avoid doing it later void waybar::Bar::setupAltFormatKeyForModule(const std::string& module_name) { if (config.isMember(module_name)) { @@ -323,48 +614,6 @@ void waybar::Bar::handleSignal(int signal) { } } -void waybar::Bar::layerSurfaceHandleConfigure(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(width, height); - 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); - wl_surface_commit(o->surface); - } - zwlr_layer_surface_v1_ack_configure(surface, serial); -} - -void waybar::Bar::layerSurfaceHandleClosed(void* data, struct zwlr_layer_surface_v1* /*surface*/) { - auto o = static_cast(data); - if (o->layer_surface_) { - zwlr_layer_surface_v1_destroy(o->layer_surface_); - o->layer_surface_ = nullptr; - } - o->modules_left_.clear(); - o->modules_center_.clear(); - o->modules_right_.clear(); -} - -auto waybar::Bar::toggle() -> void { - visible = !visible; - if (!visible) { - window.get_style_context()->add_class("hidden"); - window.set_opacity(0); - } else { - window.get_style_context()->remove_class("hidden"); - window.set_opacity(1); - } - setExclusiveZone(width_, height_); - wl_surface_commit(surface); -} - void waybar::Bar::getModules(const Factory& factory, const std::string& pos) { if (config[pos].isArray()) { for (const auto& name : config[pos]) { diff --git a/src/client.cpp b/src/client.cpp index dee28a40..95f5a295 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -1,37 +1,25 @@ #include "client.hpp" + #include #include -#include + #include + +#include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" -#include "util/json.hpp" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" waybar::Client *waybar::Client::inst() { static auto c = new Client(); return c; } -const std::string waybar::Client::getValidPath(const std::vector &paths) const { - wordexp_t p; - - for (const std::string &path : paths) { - if (wordexp(path.c_str(), &p, 0) == 0) { - if (access(*p.we_wordv, F_OK) == 0) { - std::string result = *p.we_wordv; - wordfree(&p); - return result; - } - wordfree(&p); - } - } - - return std::string(); -} - 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 && @@ -53,9 +41,9 @@ void waybar::Client::handleOutput(struct waybar_output &output) { static const struct zxdg_output_v1_listener xdgOutputListener = { .logical_position = [](void *, struct zxdg_output_v1 *, int32_t, int32_t) {}, .logical_size = [](void *, struct zxdg_output_v1 *, int32_t, int32_t) {}, - .done = [](void *, struct zxdg_output_v1 *) {}, + .done = &handleOutputDone, .name = &handleOutputName, - .description = [](void *, struct zxdg_output_v1 *, const char *) {}, + .description = &handleOutputDescription, }; // owned by output->monitor; no need to destroy auto wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); @@ -63,27 +51,6 @@ void waybar::Client::handleOutput(struct waybar_output &output) { zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output); } -bool waybar::Client::isValidOutput(const Json::Value &config, struct waybar_output &output) { - if (config["output"].isArray()) { - for (auto const &output_conf : config["output"]) { - if (output_conf.isString() && output_conf.asString() == output.name) { - return true; - } - } - return false; - } else if (config["output"].isString()) { - auto config_output_name = config["output"].asString(); - if (!config_output_name.empty()) { - if (config_output_name.substr(0, 1) == "!") { - return config_output_name.substr(1) != output.name; - } - return config_output_name == output.name; - } - } - - return true; -} - struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { auto it = std::find_if( outputs_.begin(), outputs_.end(), [&addr](const auto &output) { return &output == addr; }); @@ -94,17 +61,36 @@ struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { } std::vector waybar::Client::getOutputConfigs(struct waybar_output &output) { - std::vector configs; - if (config_.isArray()) { - for (auto const &config : config_) { - if (config.isObject() && isValidOutput(config, output)) { - configs.push_back(config); + return config.getOutputConfigs(output.name, output.identifier); +} + +void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) { + auto client = waybar::Client::inst(); + try { + auto &output = client->getOutput(data); + /** + * Multiple .done events may arrive in batch. In this case libwayland would queue + * xdg_output.destroy and dispatch all pending events, triggering this callback several times + * for the same output. .done events can also arrive after that for a scale or position changes. + * We wouldn't want to draw a duplicate bar for each such event either. + * + * All the properties we care about are immutable so it's safe to delete the xdg_output object + * on the first event and use the ptr value to check that the callback was already invoked. + */ + if (output.xdg_output) { + output.xdg_output.reset(); + spdlog::debug("Output detection done: {} ({})", output.name, output.identifier); + + auto configs = client->getOutputConfigs(output); + if (!configs.empty()) { + for (const auto &config : configs) { + client->bars.emplace_back(std::make_unique(&output, config)); + } } } - } else if (isValidOutput(config_, output)) { - configs.push_back(config_); + } catch (const std::exception &e) { + std::cerr << e.what() << std::endl; } - return configs; } void waybar::Client::handleOutputName(void * data, struct zxdg_output_v1 * /*xdg_output*/, @@ -113,22 +99,21 @@ void waybar::Client::handleOutputName(void * data, struct zxdg_output_v1 * try { auto &output = client->getOutput(data); output.name = name; - spdlog::debug("Output detected: {} ({} {})", - name, - output.monitor->get_manufacturer(), - output.monitor->get_model()); - auto configs = client->getOutputConfigs(output); - if (configs.empty()) { - output.xdg_output.reset(); - } else { - wl_display_roundtrip(client->wl_display); - for (const auto &config : configs) { - client->bars.emplace_back(std::make_unique(&output, config)); - Glib::RefPtr screen = client->bars.back()->window.get_screen(); - client->style_context_->add_provider_for_screen( - screen, client->css_provider_, GTK_STYLE_PROVIDER_PRIORITY_USER); - } - } + } catch (const std::exception &e) { + std::cerr << e.what() << std::endl; + } +} + +void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * /*xdg_output*/, + const char *description) { + 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); } catch (const std::exception &e) { std::cerr << e.what() << std::endl; } @@ -142,10 +127,21 @@ void waybar::Client::handleMonitorAdded(Glib::RefPtr monitor) { void waybar::Client::handleMonitorRemoved(Glib::RefPtr monitor) { spdlog::debug("Output removed: {} {}", monitor->get_manufacturer(), monitor->get_model()); + /* 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. + */ + Glib::signal_idle().connect_once( + sigc::bind(sigc::mem_fun(*this, &Client::handleDeferredMonitorRemoval), monitor), + Glib::PRIORITY_HIGH_IDLE); +} + +void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr monitor) { for (auto it = bars.begin(); it != bars.end();) { if ((*it)->output->monitor == monitor) { auto output_name = (*it)->output->name; - (*it)->window.close(); + (*it)->window.hide(); + gtk_app->remove_window((*it)->window); it = bars.erase(it); spdlog::info("Bar removed from output: {}", output_name); } else { @@ -155,40 +151,14 @@ void waybar::Client::handleMonitorRemoved(Glib::RefPtr monitor) { outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; }); } -std::tuple waybar::Client::getConfigs( - const std::string &config, const std::string &style) const { - auto config_file = config.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/config", - "$HOME/.config/waybar/config", - "$HOME/waybar/config", - SYSCONFDIR "/xdg/waybar/config", - "./resources/config", - }) - : config; - auto css_file = style.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/style.css", - "$HOME/.config/waybar/style.css", - "$HOME/waybar/style.css", - SYSCONFDIR "/xdg/waybar/style.css", - "./resources/style.css", - }) - : style; - if (css_file.empty() || config_file.empty()) { - throw std::runtime_error("Missing required resources files"); +const std::string waybar::Client::getStyle(const std::string &style) { + auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style; + if (!css_file) { + throw std::runtime_error("Missing required resource files"); } - spdlog::info("Resources files: {}, {}", config_file, css_file); - return {config_file, css_file}; -} - -auto waybar::Client::setupConfig(const std::string &config_file) -> void { - std::ifstream file(config_file); - if (!file.is_open()) { - throw std::runtime_error("Can't open config file"); - } - std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - util::JsonParser parser; - config_ = parser.parse(str); -} + spdlog::info("Using CSS file {}", css_file.value()); + return css_file.value(); +}; auto waybar::Client::setupCss(const std::string &css_file) -> void { css_provider_ = Gtk::CssProvider::create(); @@ -198,6 +168,9 @@ auto waybar::Client::setupCss(const std::string &css_file) -> void { if (!css_provider_->load_from_path(css_file)) { throw std::runtime_error("Can't open style file"); } + // there's always only one screen + style_context_->add_provider_for_screen( + Gdk::Screen::get_default(), css_provider_, GTK_STYLE_PROVIDER_PRIORITY_USER); } void waybar::Client::bindInterfaces() { @@ -224,14 +197,14 @@ void waybar::Client::bindInterfaces() { int waybar::Client::main(int argc, char *argv[]) { bool show_help = false; bool show_version = false; - std::string config; - std::string style; + std::string config_opt; + std::string style_opt; std::string bar_id; std::string log_level; auto cli = clara::detail::Help(show_help) | clara::detail::Opt(show_version)["-v"]["--version"]("Show version") | - clara::detail::Opt(config, "config")["-c"]["--config"]("Config path") | - clara::detail::Opt(style, "style")["-s"]["--style"]("Style path") | + clara::detail::Opt(config_opt, "config")["-c"]["--config"]("Config path") | + clara::detail::Opt(style_opt, "style")["-s"]["--style"]("Style path") | clara::detail::Opt( log_level, "trace|debug|info|warning|error|critical|off")["-l"]["--log-level"]("Log level") | @@ -252,7 +225,8 @@ int waybar::Client::main(int argc, char *argv[]) { if (!log_level.empty()) { spdlog::set_level(spdlog::level::from_str(log_level)); } - gtk_app = Gtk::Application::create(argc, argv, "fr.arouillard.waybar", Gio::APPLICATION_HANDLES_COMMAND_LINE); + gtk_app = Gtk::Application::create( + argc, argv, "fr.arouillard.waybar", Gio::APPLICATION_HANDLES_COMMAND_LINE); gdk_display = Gdk::Display::get_default(); if (!gdk_display) { throw std::runtime_error("Can't find display"); @@ -261,17 +235,16 @@ int waybar::Client::main(int argc, char *argv[]) { throw std::runtime_error("Bar need to run under Wayland"); } wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); - auto [config_file, css_file] = getConfigs(config, style); - setupConfig(config_file); + config.load(config_opt); + auto css_file = getStyle(style_opt); setupCss(css_file); bindInterfaces(); gtk_app->hold(); gtk_app->run(); bars.clear(); - zxdg_output_manager_v1_destroy(xdg_output_manager); - zwlr_layer_shell_v1_destroy(layer_shell); - zwp_idle_inhibit_manager_v1_destroy(idle_inhibit_manager); - wl_registry_destroy(registry); - wl_display_disconnect(wl_display); return 0; } + +void waybar::Client::reset() { + gtk_app->quit(); +} diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 00000000..63149cbd --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,153 @@ +#include "config.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "util/json.hpp" + +namespace waybar { + +const std::vector Config::CONFIG_DIRS = { + "$XDG_CONFIG_HOME/waybar/", + "$HOME/.config/waybar/", + "$HOME/waybar/", + "/etc/xdg/waybar/", + SYSCONFDIR "/xdg/waybar/", + "./resources/", +}; + +std::optional tryExpandPath(const std::string &path) { + 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); + return result; + } + wordfree(&p); + } + return std::nullopt; +} + +std::optional Config::findConfigPath(const std::vector &names, + const std::vector &dirs) { + std::vector paths; + for (const auto &dir : dirs) { + for (const auto &name : names) { + if (auto res = tryExpandPath(dir + name); res) { + return res; + } + } + } + return std::nullopt; +} + +void Config::setupConfig(Json::Value &dst, const std::string &config_file, int depth) { + if (depth > 100) { + throw std::runtime_error("Aborting due to likely recursive include in config files"); + } + std::ifstream file(config_file); + if (!file.is_open()) { + throw std::runtime_error("Can't open config file"); + } + std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + util::JsonParser parser; + Json::Value tmp_config = parser.parse(str); + if (tmp_config.isArray()) { + for (auto &config_part : tmp_config) { + resolveConfigIncludes(config_part, depth); + } + } else { + resolveConfigIncludes(tmp_config, depth); + } + mergeConfig(dst, tmp_config); +} + +void Config::resolveConfigIncludes(Json::Value &config, int depth) { + Json::Value includes = config["include"]; + if (includes.isArray()) { + for (const auto &include : includes) { + spdlog::info("Including resource file: {}", include.asString()); + setupConfig(config, tryExpandPath(include.asString()).value_or(""), ++depth); + } + } else if (includes.isString()) { + spdlog::info("Including resource file: {}", includes.asString()); + setupConfig(config, tryExpandPath(includes.asString()).value_or(""), ++depth); + } +} + +void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) { + if (!a_config_) { + // For the first config + a_config_ = b_config_; + } else if (a_config_.isObject() && b_config_.isObject()) { + for (const auto &key : b_config_.getMemberNames()) { + // [] creates key with default value. Use `get` to avoid that. + if (a_config_.get(key, Json::Value::nullSingleton()).isObject() && + b_config_[key].isObject()) { + mergeConfig(a_config_[key], b_config_[key]); + } else if (!a_config_.isMember(key)) { + // do not allow overriding value set by top or previously included config + a_config_[key] = b_config_[key]; + } else { + spdlog::trace("Option {} is already set; ignoring value {}", key, b_config_[key]); + } + } + } else { + spdlog::error("Cannot merge config, conflicting or invalid JSON types"); + } +} +bool isValidOutput(const Json::Value &config, const std::string &name, + const std::string &identifier) { + if (config["output"].isArray()) { + for (auto const &output_conf : config["output"]) { + if (output_conf.isString() && + (output_conf.asString() == name || output_conf.asString() == identifier)) { + return true; + } + } + return false; + } else if (config["output"].isString()) { + auto config_output = config["output"].asString(); + if (!config_output.empty()) { + if (config_output.substr(0, 1) == "!") { + return config_output.substr(1) != name && config_output.substr(1) != identifier; + } + return config_output == name || config_output == identifier; + } + } + + return true; +} + +void Config::load(const std::string &config) { + auto file = config.empty() ? findConfigPath({"config", "config.jsonc"}) : config; + if (!file) { + throw std::runtime_error("Missing required resource files"); + } + config_file_ = file.value(); + spdlog::info("Using configuration file {}", config_file_); + setupConfig(config_, config_file_, 0); +} + +std::vector Config::getOutputConfigs(const std::string &name, + const std::string &identifier) { + std::vector configs; + if (config_.isArray()) { + for (auto const &config : config_) { + if (config.isObject() && isValidOutput(config, name, identifier)) { + configs.push_back(config); + } + } + } else if (isValidOutput(config_, name, identifier)) { + configs.push_back(config_); + } + return configs; +} + +} // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index 6005cad5..a577751a 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -22,6 +22,22 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "sway/window") { return new waybar::modules::sway::Window(id, bar_, config_[name]); } + if (ref == "sway/language") { + return new waybar::modules::sway::Language(id, config_[name]); + } +#endif +#ifdef HAVE_WLR + if (ref == "wlr/taskbar") { + return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); + } + if (ref == "wlr/workspaces") { + return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]); + } +#endif +#ifdef HAVE_RIVER + if (ref == "river/tags") { + return new waybar::modules::river::Tags(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); @@ -57,6 +73,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::Backlight(id, config_[name]); } #endif +#ifdef HAVE_LIBEVDEV + if (ref == "keyboard-state") { + return new waybar::modules::KeyboardState(id, bar_, config_[name]); + } +#endif #ifdef HAVE_LIBPULSE if (ref == "pulseaudio") { return new waybar::modules::Pulseaudio(id, config_[name]); @@ -66,14 +87,21 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "mpd") { return new waybar::modules::MPD(id, config_[name]); } +#endif +#ifdef HAVE_LIBSNDIO + if (ref == "sndio") { + return new waybar::modules::Sndio(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); } #if defined(__linux__) +# ifdef WANT_RFKILL if (ref == "bluetooth") { return new waybar::modules::Bluetooth(id, config_[name]); } +# endif #endif if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); diff --git a/src/main.cpp b/src/main.cpp index f066cf85..13a25670 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,16 +1,89 @@ #include +#include +#include +#include +#include #include #include "client.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); + + while (true) { + err = sigwait(&mask, &signum); + if (err != 0) { + spdlog::error("sigwait failed: {}", strerror(errno)); + 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; + } + } +} + +void startSignalThread(void) { + int err; + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + + // 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); + } + + 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); + } +} + int main(int argc, char* argv[]) { try { auto client = waybar::Client::inst(); + std::signal(SIGUSR1, [](int /*signal*/) { for (auto& bar : waybar::Client::inst()->bars) { bar->toggle(); } }); + std::signal(SIGUSR2, [](int /*signal*/) { + spdlog::info("Reloading..."); + reload = true; + waybar::Client::inst()->reset(); + }); + for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { std::signal(sig, [](int sig) { for (auto& bar : waybar::Client::inst()->bars) { @@ -18,8 +91,14 @@ int main(int argc, char* argv[]) { } }); } + startSignalThread(); + + auto ret = 0; + do { + reload = false; + ret = client->main(argc, argv); + } while (reload); - auto ret = client->main(argc, argv); delete client; return ret; } catch (const std::exception& e) { diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 3ebc6e7b..fcd668c5 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -173,7 +173,7 @@ auto waybar::modules::Backlight::update() -> void { return; } - const auto percent = best->get_max() == 0 ? 100 : best->get_actual() * 100 / best->get_max(); + const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); label_.set_markup(fmt::format( format_, fmt::arg("percent", std::to_string(percent)), fmt::arg("icon", getIcon(percent)))); getState(percent); diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 406b4a33..26567690 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -4,46 +4,82 @@ waybar::modules::Battery::Battery(const std::string& id, const Json::Value& config) : ALabel(config, "battery", id, "{capacity}%", 60) { - getBatteries(); - fd_ = inotify_init1(IN_CLOEXEC); - if (fd_ == -1) { + battery_watch_fd_ = inotify_init1(IN_CLOEXEC); + if (battery_watch_fd_ == -1) { throw std::runtime_error("Unable to listen batteries."); } - for (auto const& bat : batteries_) { - auto wd = inotify_add_watch(fd_, (bat / "uevent").c_str(), IN_ACCESS); - if (wd != -1) { - wds_.push_back(wd); - } + + global_watch_fd_ = inotify_init1(IN_CLOEXEC); + if (global_watch_fd_ == -1) { + throw std::runtime_error("Unable to listen batteries."); } + + // Watch the directory for any added or removed batteries + global_watch = inotify_add_watch(global_watch_fd_, data_dir_.c_str(), IN_CREATE | IN_DELETE); + if (global_watch < 0) { + throw std::runtime_error("Could not watch for battery plug/unplug"); + } + + refreshBatteries(); worker(); } waybar::modules::Battery::~Battery() { - for (auto wd : wds_) { - inotify_rm_watch(fd_, wd); + std::lock_guard guard(battery_list_mutex_); + + if (global_watch >= 0) { + inotify_rm_watch(global_watch_fd_, global_watch); } - close(fd_); + close(global_watch_fd_); + + for (auto it = batteries_.cbegin(); it != batteries_.cend(); it++) { + auto watch_id = (*it).second; + if (watch_id >= 0) { + inotify_rm_watch(battery_watch_fd_, watch_id); + } + batteries_.erase(it); + } + close(battery_watch_fd_); } void waybar::modules::Battery::worker() { thread_timer_ = [this] { + // Make sure we eventually update the list of batteries even if we miss an + // inotify event for some reason + refreshBatteries(); dp.emit(); thread_timer_.sleep_for(interval_); }; thread_ = [this] { struct inotify_event event = {0}; - int nbytes = read(fd_, &event, sizeof(event)); + int nbytes = read(battery_watch_fd_, &event, sizeof(event)); if (nbytes != sizeof(event) || event.mask & IN_IGNORED) { thread_.stop(); return; } - // TODO: don't stop timer for now since there is some bugs :? - // thread_timer_.stop(); + dp.emit(); + }; + thread_battery_update_ = [this] { + struct inotify_event event = {0}; + int nbytes = read(global_watch_fd_, &event, sizeof(event)); + if (nbytes != sizeof(event) || event.mask & IN_IGNORED) { + thread_.stop(); + return; + } + refreshBatteries(); dp.emit(); }; } -void waybar::modules::Battery::getBatteries() { +void waybar::modules::Battery::refreshBatteries() { + std::lock_guard guard(battery_list_mutex_); + + // Mark existing list of batteries as not necessarily found + std::map check_map; + for (auto const& bat : batteries_) { + check_map[bat.first] = false; + } + try { for (auto& node : fs::directory_iterator(data_dir_)) { if (!fs::is_directory(node)) { @@ -53,8 +89,23 @@ void waybar::modules::Battery::getBatteries() { auto bat_defined = config_["bat"].isString(); if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && fs::exists(node.path() / "capacity") && fs::exists(node.path() / "uevent") && - fs::exists(node.path() / "status")) { - batteries_.push_back(node.path()); + fs::exists(node.path() / "status") && fs::exists(node.path() / "type")) { + std::string type; + std::ifstream(node.path() / "type") >> type; + + if (!type.compare("Battery")){ + check_map[node.path()] = true; + auto search = batteries_.find(node.path()); + if (search == batteries_.end()) { + // We've found a new battery save it and start listening for events + auto event_path = (node.path() / "uevent"); + auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS); + if (wd < 0) { + throw std::runtime_error("Could not watch events for " + node.path().string()); + } + batteries_[node.path()] = wd; + } + } } auto adap_defined = config_["adapter"].isString(); if (((adap_defined && dir_name == config_["adapter"].asString()) || !adap_defined) && @@ -71,36 +122,86 @@ void waybar::modules::Battery::getBatteries() { } throw std::runtime_error("No batteries."); } + + // Remove any batteries that are no longer present and unwatch them + for (auto const& check : check_map) { + if (!check.second) { + auto watch_id = batteries_[check.first]; + if (watch_id >= 0) { + inotify_rm_watch(battery_watch_fd_, watch_id); + } + batteries_.erase(check.first); + } + } } -const std::tuple waybar::modules::Battery::getInfos() const { +// Unknown > Full > Not charging > Discharging > Charging +static bool status_gt(const std::string& a, const std::string& b) { + if (a == b) return false; + else if (a == "Unknown") return true; + else if (a == "Full" && b != "Unknown") return true; + else if (a == "Not charging" && b != "Unknown" && b != "Full") return true; + else if (a == "Discharging" && b != "Unknown" && b != "Full" && b != "Not charging") return true; + return false; +} + +const std::tuple waybar::modules::Battery::getInfos() { + std::lock_guard guard(battery_list_mutex_); + try { - uint16_t total = 0; uint32_t total_power = 0; // μW uint32_t total_energy = 0; // μWh uint32_t total_energy_full = 0; + uint32_t total_energy_full_design = 0; std::string status = "Unknown"; - for (auto const& bat : batteries_) { - uint16_t capacity; + for (auto const& item : batteries_) { + auto bat = item.first; uint32_t power_now; uint32_t energy_full; uint32_t energy_now; + uint32_t energy_full_design; std::string _status; - std::ifstream(bat / "capacity") >> capacity; std::ifstream(bat / "status") >> _status; - auto rate_path = fs::exists(bat / "current_now") ? "current_now" : "power_now"; - std::ifstream(bat / rate_path) >> power_now; - auto now_path = fs::exists(bat / "charge_now") ? "charge_now" : "energy_now"; - std::ifstream(bat / now_path) >> energy_now; - auto full_path = fs::exists(bat / "charge_full") ? "charge_full" : "energy_full"; - std::ifstream(bat / full_path) >> energy_full; - if (_status != "Unknown") { + + // Some battery will report current and charge in μA/μAh. + // Scale these by the voltage to get μW/μWh. + if (fs::exists(bat / "current_now")) { + uint32_t voltage_now; + uint32_t current_now; + uint32_t charge_now; + uint32_t charge_full; + uint32_t charge_full_design; + std::ifstream(bat / "voltage_now") >> voltage_now; + std::ifstream(bat / "current_now") >> current_now; + std::ifstream(bat / "charge_full") >> charge_full; + std::ifstream(bat / "charge_full_design") >> charge_full_design; + if (fs::exists(bat / "charge_now")) + std::ifstream(bat / "charge_now") >> charge_now; + else { + // charge_now is missing on some systems, estimate using capacity. + uint32_t capacity; + std::ifstream(bat / "capacity") >> capacity; + charge_now = (capacity * charge_full) / 100; + } + power_now = ((uint64_t)current_now * (uint64_t)voltage_now) / 1000000; + energy_now = ((uint64_t)charge_now * (uint64_t)voltage_now) / 1000000; + energy_full = ((uint64_t)charge_full * (uint64_t)voltage_now) / 1000000; + energy_full_design = ((uint64_t)charge_full_design * (uint64_t)voltage_now) / 1000000; + } else { + std::ifstream(bat / "power_now") >> power_now; + std::ifstream(bat / "energy_now") >> energy_now; + std::ifstream(bat / "energy_full") >> energy_full; + std::ifstream(bat / "energy_full_design") >> energy_full_design; + } + + // Show the "smallest" status among all batteries + if (status_gt(status, _status)) { status = _status; } - total += capacity; total_power += power_now; total_energy += energy_now; total_energy_full += energy_full; + total_energy_full_design += energy_full_design; } if (!adapter_.empty() && status == "Discharging") { bool online; @@ -114,22 +215,40 @@ const std::tuple waybar::modules::Battery::getInfos time_remaining = (float)total_energy / total_power; } else if (status == "Charging" && total_power != 0) { time_remaining = -(float)(total_energy_full - total_energy) / total_power; + if (time_remaining > 0.0f) { + // If we've turned positive it means the battery is past 100% and so + // just report that as no time remaining + time_remaining = 0.0f; + } + } + float capacity = ((float)total_energy * 100.0f / (float) total_energy_full); + // Handle design-capacity + if (config_["design-capacity"].isBool() ? config_["design-capacity"].asBool() : false) { + capacity = ((float)total_energy * 100.0f / (float) total_energy_full_design); } - uint16_t capacity = total / batteries_.size(); // Handle full-at if (config_["full-at"].isUInt()) { auto full_at = config_["full-at"].asUInt(); if (full_at < 100) { capacity = 100.f * capacity / full_at; - if (capacity > full_at) { - capacity = full_at; - } } } - return {capacity, time_remaining, status}; + if (capacity > 100.f) { + // This can happen when the battery is calibrating and goes above 100% + // Handle it gracefully by clamping at 100% + capacity = 100.f; + } + uint8_t cap = round(capacity); + if (cap == 100 && status == "Charging") { + // If we've reached 100% just mark as full as some batteries can stay + // stuck reporting they're still charging but not yet done + status = "Full"; + } + + return {cap, time_remaining, status, total_power / 1e6}; } catch (const std::exception& e) { spdlog::error("Battery: {}", e.what()); - return {0, 0, "Unknown"}; + return {0, 0, "Unknown", 0}; } } @@ -153,6 +272,10 @@ const std::string waybar::modules::Battery::formatTimeRemaining(float hoursRemai uint16_t full_hours = static_cast(hoursRemaining); uint16_t minutes = static_cast(60 * (hoursRemaining - full_hours)); auto format = std::string("{H} h {M} min"); + if (full_hours == 0 && minutes == 0) { + // Migh as well not show "0h 0min" + return ""; + } if (config_["format-time"].isString()) { format = config_["format-time"].asString(); } @@ -160,26 +283,41 @@ const std::string waybar::modules::Battery::formatTimeRemaining(float hoursRemai } auto waybar::modules::Battery::update() -> void { - auto [capacity, time_remaining, status] = getInfos(); + auto [capacity, time_remaining, status, power] = getInfos(); if (status == "Unknown") { status = getAdapterStatus(capacity); } - if (tooltipEnabled()) { - std::string tooltip_text; - if (time_remaining != 0) { - std::string time_to = std::string("Time to ") + ((time_remaining > 0) ? "empty" : "full"); - tooltip_text = time_to + ": " + formatTimeRemaining(time_remaining); - } else { - tooltip_text = status; - } - label_.set_tooltip_text(tooltip_text); - } + auto status_pretty = status; // Transform to lowercase and replace space with dash std::transform(status.begin(), status.end(), status.begin(), [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; auto state = getState(capacity, true); + auto time_remaining_formatted = formatTimeRemaining(time_remaining); + if (tooltipEnabled()) { + std::string tooltip_text_default; + std::string tooltip_format = "{timeTo}"; + if (time_remaining != 0) { + std::string time_to = std::string("Time to ") + ((time_remaining > 0) ? "empty" : "full"); + tooltip_text_default = time_to + ": " + time_remaining_formatted; + } else { + tooltip_text_default = status_pretty; + } + if (!state.empty() && config_["tooltip-format-" + status + "-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + status + "-" + state].asString(); + } else if (config_["tooltip-format-" + status].isString()) { + tooltip_format = config_["tooltip-format-" + status].asString(); + } else if (!state.empty() && config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } else if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + label_.set_tooltip_text(fmt::format(tooltip_format, + fmt::arg("timeTo", tooltip_text_default), + fmt::arg("capacity", capacity), + fmt::arg("time", time_remaining_formatted))); + } if (!old_status_.empty()) { label_.get_style_context()->remove_class(old_status_); } @@ -199,8 +337,9 @@ auto waybar::modules::Battery::update() -> void { auto icons = std::vector{status + "-" + state, status, state}; label_.set_markup(fmt::format(format, fmt::arg("capacity", capacity), + fmt::arg("power", power), fmt::arg("icon", getIcon(capacity, icons)), - fmt::arg("time", formatTimeRemaining(time_remaining)))); + fmt::arg("time", time_remaining_formatted))); } // Call parent update ALabel::update(); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index b390976a..1540c053 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -1,45 +1,25 @@ #include "modules/bluetooth.hpp" -#include "util/rfkill.hpp" -#include -#include + +#include waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config) - : ALabel(config, "bluetooth", id, "{icon}", 10), - status_("disabled"), - rfkill_{RFKILL_TYPE_BLUETOOTH} { - thread_ = [this] { - dp.emit(); - rfkill_.waitForEvent(); - }; - intervall_thread_ = [this] { - auto now = std::chrono::system_clock::now(); - auto timeout = std::chrono::floor(now + interval_); - auto diff = std::chrono::seconds(timeout.time_since_epoch().count() % interval_.count()); - thread_.sleep_until(timeout - diff); - dp.emit(); - }; + : ALabel(config, "bluetooth", id, "{icon}", 10), rfkill_{RFKILL_TYPE_BLUETOOTH} { + rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); } auto waybar::modules::Bluetooth::update() -> void { - if (rfkill_.getState()) { - status_ = "disabled"; - } else { - status_ = "enabled"; - } + std::string status = rfkill_.getState() ? "disabled" : "enabled"; label_.set_markup( - fmt::format( - format_, - fmt::arg("status", status_), - fmt::arg("icon", getIcon(0, status_)))); + fmt::format(format_, fmt::arg("status", status), fmt::arg("icon", getIcon(0, status)))); if (tooltipEnabled()) { if (config_["tooltip-format"].isString()) { auto tooltip_format = config_["tooltip-format"].asString(); - auto tooltip_text = fmt::format(tooltip_format, status_); + auto tooltip_text = fmt::format(tooltip_format, status, fmt::arg("status", status)); label_.set_tooltip_text(tooltip_text); } else { - label_.set_tooltip_text(status_); + label_.set_tooltip_text(status); } } } diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index f41126b0..7c94c457 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -5,6 +5,7 @@ #include #include +#include "util/ustring_clen.hpp" #ifdef HAVE_LANGINFO_1STDAY #include #include @@ -13,11 +14,15 @@ using waybar::modules::waybar_time; waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) - : ALabel(config, "clock", id, "{:%H:%M}", 60), fixed_time_zone_(false) { - if (config_["timezone"].isString()) { - spdlog::warn("As using a timezone, some format args may be missing as the date library havn't got a release since 2018."); - time_zone_ = date::locate_zone(config_["timezone"].asString()); - fixed_time_zone_ = true; + : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), fixed_time_zone_(false) { + if (config_["timezones"].isArray() && !config_["timezones"].empty()) { + time_zone_idx_ = 0; + setTimeZone(config_["timezones"][time_zone_idx_]); + } else { + setTimeZone(config_["timezone"]); + } + if (fixed_time_zone_) { + spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018."); } if (config_["locale"].isString()) { @@ -71,6 +76,42 @@ auto waybar::modules::Clock::update() -> void { ALabel::update(); } +bool waybar::modules::Clock::setTimeZone(Json::Value zone_name) { + if (!zone_name.isString() || zone_name.asString().empty()) { + fixed_time_zone_ = false; + return false; + } + + time_zone_ = date::locate_zone(zone_name.asString()); + fixed_time_zone_ = true; + return true; +} + +bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) { + // defer to user commands if set + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + + auto dir = AModule::getScrollDir(e); + if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) { + return true; + } + if (!config_["timezones"].isArray() || config_["timezones"].empty()) { + return true; + } + auto nr_zones = config_["timezones"].size(); + if (dir == SCROLL_DIR::UP) { + size_t new_idx = time_zone_idx_ + 1; + time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; + } else { + time_zone_idx_ = time_zone_idx_ == 0 ? nr_zones - 1 : time_zone_idx_ - 1; + } + setTimeZone(config_["timezones"][time_zone_idx_]); + update(); + return true; +} + auto waybar::modules::Clock::calendar_text(const waybar_time& wtime) -> std::string { const auto daypoint = date::floor(wtime.ztime.get_local_time()); const auto ymd = date::year_month_day(daypoint); @@ -99,7 +140,12 @@ auto waybar::modules::Clock::calendar_text(const waybar_time& wtime) -> std::str os << '\n'; } if (d == curr_day) { - os << "" << date::format("%e", d) << ""; + if (config_["today-format"].isString()) { + auto today_format = config_["today-format"].asString(); + os << fmt::format(today_format, date::format("%e", d)); + } else { + os << "" << date::format("%e", d) << ""; + } } else { os << date::format("%e", d); } @@ -117,12 +163,14 @@ auto waybar::modules::Clock::weekdays_header(const date::weekday& first_dow, std do { if (wd != first_dow) os << ' '; Glib::ustring wd_ustring(date::format(locale_, "%a", wd)); - auto wd_len = wd_ustring.length(); - if (wd_len > 2) { - wd_ustring = wd_ustring.substr(0, 2); - wd_len = 2; + auto clen = ustring_clen(wd_ustring); + auto wd_len = wd_ustring.length(); + while (clen > 2) { + wd_ustring = wd_ustring.substr(0, wd_len-1); + wd_len--; + clen = ustring_clen(wd_ustring); } - const std::string pad(2 - wd_len, ' '); + const std::string pad(2 - clen, ' '); os << pad << wd_ustring; } while (++wd != first_dow); os << "\n"; @@ -156,6 +204,9 @@ template <> struct fmt::formatter : fmt::formatter { template auto format(const waybar_time& t, FormatContext& ctx) { +#if FMT_VERSION >= 80000 + auto& tm_format = specs; +#endif return format_to(ctx.out(), "{}", date::format(t.locale, fmt::to_string(tm_format), t.ztime)); } }; diff --git a/src/modules/cpu/bsd.cpp b/src/modules/cpu/bsd.cpp index 73ab1e8a..a92252f1 100644 --- a/src/modules/cpu/bsd.cpp +++ b/src/modules/cpu/bsd.cpp @@ -2,8 +2,10 @@ #include #include +#include #include // malloc #include // sysconf +#include // NAN #if defined(__NetBSD__) || defined(__OpenBSD__) # include @@ -95,3 +97,12 @@ std::vector> waybar::modules::Cpu::parseCpuinfo() { 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/common.cpp index f2204cde..4cf67eba 100644 --- a/src/modules/cpu/common.cpp +++ b/src/modules/cpu/common.cpp @@ -1,5 +1,14 @@ #include "modules/cpu.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] { @@ -12,31 +21,60 @@ auto waybar::modules::Cpu::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(); if (tooltipEnabled()) { label_.set_tooltip_text(tooltip); } - label_.set_markup(fmt::format(format_, fmt::arg("load", cpu_load), fmt::arg("usage", cpu_usage))); - getState(cpu_usage); + 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", cpu_load)); + store.push_back(fmt::arg("load", cpu_load)); + store.push_back(fmt::arg("usage", total_usage)); + store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + 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(); } -uint16_t waybar::modules::Cpu::getCpuLoad() { +double waybar::modules::Cpu::getCpuLoad() { double load[1]; if (getloadavg(load, 1) != -1) { - return load[0] * 100 / sysconf(_SC_NPROCESSORS_ONLN); + return load[0]; } throw std::runtime_error("Can't get Cpu load"); } -std::tuple waybar::modules::Cpu::getCpuUsage() { +std::tuple, std::string> waybar::modules::Cpu::getCpuUsage() { if (prev_times_.empty()) { prev_times_ = parseCpuinfo(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } std::vector> curr_times = parseCpuinfo(); std::string tooltip; - uint16_t usage = 0; + std::vector usage; for (size_t i = 0; i < curr_times.size(); ++i) { auto [curr_idle, curr_total] = curr_times[i]; auto [prev_idle, prev_total] = prev_times_[i]; @@ -44,12 +82,25 @@ std::tuple waybar::modules::Cpu::getCpuUsage() { const float delta_total = curr_total - prev_total; uint16_t tmp = 100 * (1 - delta_idle / delta_total); if (i == 0) { - usage = tmp; tooltip = fmt::format("Total: {}%", tmp); } else { tooltip = tooltip + fmt::format("\nCore{}: {}%", i - 1, tmp); } + usage.push_back(tmp); } prev_times_ = curr_times; return {usage, tooltip}; } + +std::tuple waybar::modules::Cpu::getCpuFrequency() { + std::vector frequencies = parseCpuFrequencies(); + 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/linux.cpp index 9f1734fb..6d638a9f 100644 --- a/src/modules/cpu/linux.cpp +++ b/src/modules/cpu/linux.cpp @@ -1,3 +1,4 @@ +#include #include "modules/cpu.hpp" std::vector> waybar::modules::Cpu::parseCpuinfo() { @@ -27,3 +28,50 @@ std::vector> waybar::modules::Cpu::parseCpuinfo() { } return cpuinfo; } + +std::vector waybar::modules::Cpu::parseCpuFrequencies() { + const std::string file_path_ = "/proc/cpuinfo"; + std::ifstream info(file_path_); + if (!info.is_open()) { + throw std::runtime_error("Can't open " + file_path_); + } + std::vector frequencies; + std::string line; + while (getline(info, line)) { + if (line.substr(0, 7).compare("cpu MHz") != 0) { + continue; + } + + std::string frequency_str = line.substr(line.find(":") + 2); + float frequency = std::strtol(frequency_str.c_str(), nullptr, 10); + frequencies.push_back(frequency); + } + info.close(); + + if (frequencies.size() <= 0) { + std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq"; + if (std::filesystem::exists(cpufreq_dir)) { + std::vector frequency_files = { + "/cpuinfo_min_freq", + "/cpuinfo_max_freq" + }; + for (auto& p: std::filesystem::directory_iterator(cpufreq_dir)) { + for (auto freq_file: frequency_files) { + std::string freq_file_path = p.path().string() + freq_file; + if (std::filesystem::exists(freq_file_path)) { + std::string freq_value; + std::ifstream freq(freq_file_path); + if (freq.is_open()) { + getline(freq, freq_value); + float frequency = std::strtol(freq_value.c_str(), nullptr, 10); + frequencies.push_back(frequency / 1000); + freq.close(); + } + } + } + } + } + } + + return frequencies; +} diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 7ab7a066..ba55edd5 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -50,7 +50,6 @@ void waybar::modules::Custom::continuousWorker() { thread_ = [this, cmd] { char* buff = nullptr; size_t len = 0; - bool restart = false; if (getline(&buff, &len, fp_) == -1) { int exit_code = 1; if (fp_) { @@ -63,8 +62,8 @@ void waybar::modules::Custom::continuousWorker() { spdlog::error("{} stopped unexpectedly, is it endless?", name_); } if (config_["restart-interval"].isUInt()) { - restart = true; pid_ = -1; + thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); fp_ = util::command::open(cmd, pid_); if (!fp_) { throw std::runtime_error("Unable to open " + cmd); @@ -83,9 +82,6 @@ void waybar::modules::Custom::continuousWorker() { output_ = {0, output}; dp.emit(); } - if (restart) { - thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); - } }; } @@ -95,15 +91,21 @@ void waybar::modules::Custom::refresh(int sig) { } } +void waybar::modules::Custom::handleEvent() { + if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) { + thread_.wake_up(); + } +} + bool waybar::modules::Custom::handleScroll(GdkEventScroll* e) { auto ret = ALabel::handleScroll(e); - thread_.wake_up(); + handleEvent(); return ret; } bool waybar::modules::Custom::handleToggle(GdkEventButton* const& e) { auto ret = ALabel::handleToggle(e); - thread_.wake_up(); + handleEvent(); return ret; } @@ -129,9 +131,13 @@ auto waybar::modules::Custom::update() -> void { label_.set_markup(str); if (tooltipEnabled()) { if (text_ == tooltip_) { - label_.set_tooltip_text(str); + if (label_.get_tooltip_markup() != str) { + label_.set_tooltip_markup(str); + } } else { - label_.set_tooltip_text(tooltip_); + if (label_.get_tooltip_markup() != tooltip_) { + label_.set_tooltip_markup(tooltip_); + } } } auto classes = label_.get_style_context()->list_classes(); diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index 658fcd90..b4e50533 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -47,16 +47,29 @@ auto waybar::modules::Disk::update() -> void { 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); + auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; + + auto format = format_; + auto state = getState(percentage_used); + if (!state.empty() && config_["format-" + state].isString()) { + format = config_["format-" + state].asString(); + } + + if (format.empty()) { + event_box_.hide(); + } else { + event_box_.show(); + label_.set_markup(fmt::format(format + , stats.f_bavail * 100 / stats.f_blocks + , fmt::arg("free", free) + , fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks) + , fmt::arg("used", used) + , fmt::arg("percentage_used", percentage_used) + , fmt::arg("total", total) + , fmt::arg("path", path_) + )); + } - label_.set_markup(fmt::format(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", (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks) - , fmt::arg("total", total) - , fmt::arg("path", path_) - )); if (tooltipEnabled()) { std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)"; if (config_["tooltip-format"].isString()) { @@ -67,12 +80,11 @@ auto waybar::modules::Disk::update() -> void { , fmt::arg("free", free) , fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks) , fmt::arg("used", used) - , fmt::arg("percentage_used", (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks) + , fmt::arg("percentage_used", percentage_used) , fmt::arg("total", total) , fmt::arg("path", path_) )); } - event_box_.show(); // Call parent update ALabel::update(); } diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index d94e9579..26889c23 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -1,16 +1,28 @@ #include "modules/idle_inhibitor.hpp" + +#include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/command.hpp" +std::list waybar::modules::IdleInhibitor::modules; +bool waybar::modules::IdleInhibitor::status = false; + waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "idle_inhibitor", id, "{status}"), bar_(bar), - status_("deactivated"), idle_inhibitor_(nullptr), pid_(-1) { + if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { + throw std::runtime_error("idle-inhibit not available"); + } + event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleToggle)); + + // Add this to the modules list + waybar::modules::IdleInhibitor::modules.push_back(this); + dp.emit(); } @@ -19,6 +31,10 @@ waybar::modules::IdleInhibitor::~IdleInhibitor() { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); idle_inhibitor_ = nullptr; } + + // Remove this from the modules list + waybar::modules::IdleInhibitor::modules.remove(this); + if (pid_ != -1) { kill(-pid_, 9); pid_ = -1; @@ -26,11 +42,27 @@ waybar::modules::IdleInhibitor::~IdleInhibitor() { } auto waybar::modules::IdleInhibitor::update() -> void { + // Check status + if (status) { + label_.get_style_context()->remove_class("deactivated"); + if (idle_inhibitor_ == nullptr) { + idle_inhibitor_ = zwp_idle_inhibit_manager_v1_create_inhibitor( + waybar::Client::inst()->idle_inhibit_manager, bar_.surface); + } + } else { + label_.get_style_context()->remove_class("activated"); + if (idle_inhibitor_ != nullptr) { + zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); + idle_inhibitor_ = nullptr; + } + } + + std::string status_text = status ? "activated" : "deactivated"; label_.set_markup( - fmt::format(format_, fmt::arg("status", status_), fmt::arg("icon", getIcon(0, status_)))); - label_.get_style_context()->add_class(status_); + fmt::format(format_, fmt::arg("status", status_text), fmt::arg("icon", getIcon(0, status_text)))); + label_.get_style_context()->add_class(status_text); if (tooltipEnabled()) { - label_.set_tooltip_text(status_); + label_.set_tooltip_text(status_text); } // Call parent update ALabel::update(); @@ -38,18 +70,16 @@ auto waybar::modules::IdleInhibitor::update() -> void { bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { if (e->button == 1) { - label_.get_style_context()->remove_class(status_); - if (idle_inhibitor_ != nullptr) { - zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); - idle_inhibitor_ = nullptr; - status_ = "deactivated"; - } else { - idle_inhibitor_ = zwp_idle_inhibit_manager_v1_create_inhibitor( - waybar::Client::inst()->idle_inhibit_manager, bar_.surface); - status_ = "activated"; + status = !status; + + // Make all other idle inhibitor modules update + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + if (module != this) { + module->update(); + } } - click_param = status_; } + ALabel::handleToggle(e); return true; } diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp new file mode 100644 index 00000000..2b6eb2d9 --- /dev/null +++ b/src/modules/keyboard_state.cpp @@ -0,0 +1,152 @@ +#include "modules/keyboard_state.hpp" +#include +#include + +extern "C" { +#include +#include +#include +} + +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), + numlock_label_(""), + capslock_label_(""), + numlock_format_(config_["format"].isString() ? config_["format"].asString() + : config_["format"]["numlock"].isString() ? config_["format"]["numlock"].asString() + : "{name} {icon}"), + capslock_format_(config_["format"].isString() ? config_["format"].asString() + : config_["format"]["capslock"].isString() ? config_["format"]["capslock"].asString() + : "{name} {icon}"), + scrolllock_format_(config_["format"].isString() ? config_["format"].asString() + : config_["format"]["scrolllock"].isString() ? config_["format"]["scrolllock"].asString() + : "{name} {icon}"), + interval_(std::chrono::seconds(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1)), + icon_locked_(config_["format-icons"]["locked"].isString() + ? config_["format-icons"]["locked"].asString() + : "locked"), + icon_unlocked_(config_["format-icons"]["unlocked"].isString() + ? config_["format-icons"]["unlocked"].asString() + : "unlocked"), + fd_(0), + dev_(nullptr) { + box_.set_name("keyboard-state"); + if (config_["numlock"].asBool()) { + box_.pack_end(numlock_label_, false, false, 0); + } + if (config_["capslock"].asBool()) { + box_.pack_end(capslock_label_, false, false, 0); + } + if (config_["scrolllock"].asBool()) { + box_.pack_end(scrolllock_label_, false, false, 0); + } + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + if (config_["device-path"].isString()) { + std::string dev_path = config_["device-path"].asString(); + std::tie(fd_, dev_) = openDevice(dev_path); + } else { + DIR* dev_dir = opendir("/dev/input"); + if (dev_dir == nullptr) { + throw std::runtime_error("Failed to open /dev/input"); + } + dirent *ep; + while ((ep = readdir(dev_dir))) { + if (ep->d_type != DT_CHR) continue; + std::string dev_path = std::string("/dev/input/") + ep->d_name; + try { + std::tie(fd_, dev_) = openDevice(dev_path); + spdlog::info("Found device {} at '{}'", libevdev_get_name(dev_), dev_path); + break; + } catch (const std::runtime_error& e) { + continue; + } + } + if (dev_ == nullptr) { + throw std::runtime_error("Failed to find keyboard device"); + } + } + + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +waybar::modules::KeyboardState::~KeyboardState() { + libevdev_free(dev_); + int err = close(fd_); + if (err < 0) { + // Not much we can do, so ignore it. + } +} + +auto waybar::modules::KeyboardState::openDevice(const std::string& path) -> std::pair { + int fd = open(path.c_str(), O_NONBLOCK | O_CLOEXEC | O_RDONLY); + if (fd < 0) { + throw std::runtime_error("Can't open " + path); + } + + libevdev* dev; + int err = libevdev_new_from_fd(fd, &dev); + if (err < 0) { + throw std::runtime_error("Can't create libevdev device"); + } + if (!libevdev_has_event_type(dev, EV_LED)) { + throw std::runtime_error("Device doesn't support LED events"); + } + if (!libevdev_has_event_code(dev, EV_LED, LED_NUML) + || !libevdev_has_event_code(dev, EV_LED, LED_CAPSL) + || !libevdev_has_event_code(dev, EV_LED, LED_SCROLLL)) { + throw std::runtime_error("Device doesn't support num lock, caps lock, or scroll lock events"); + } + + return std::make_pair(fd, dev); +} + +auto waybar::modules::KeyboardState::update() -> void { + int err = LIBEVDEV_READ_STATUS_SUCCESS; + while (err == LIBEVDEV_READ_STATUS_SUCCESS) { + input_event ev; + err = libevdev_next_event(dev_, LIBEVDEV_READ_FLAG_NORMAL, &ev); + while (err == LIBEVDEV_READ_STATUS_SYNC) { + err = libevdev_next_event(dev_, LIBEVDEV_READ_FLAG_SYNC, &ev); + } + } + if (err != -EAGAIN) { + throw std::runtime_error("Failed to sync evdev device"); + } + + int numl = libevdev_get_event_value(dev_, EV_LED, LED_NUML); + int capsl = libevdev_get_event_value(dev_, EV_LED, LED_CAPSL); + int scrolll = libevdev_get_event_value(dev_, EV_LED, LED_SCROLLL); + + struct { + bool state; + Gtk::Label& label; + const std::string& format; + const char* name; + } label_states[] = { + {(bool) numl, numlock_label_, numlock_format_, "Num"}, + {(bool) capsl, capslock_label_, capslock_format_, "Caps"}, + {(bool) scrolll, scrolllock_label_, scrolllock_format_, "Scroll"}, + }; + for (auto& label_state : label_states) { + std::string text; + text = fmt::format(label_state.format, + fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_), + fmt::arg("name", label_state.name)); + label_state.label.set_markup(text); + if (label_state.state) { + label_state.label.get_style_context()->add_class("locked"); + } else { + label_state.label.get_style_context()->remove_class("locked"); + } + } + + AModule::update(); +} diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 4875ec8f..75e05302 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -15,11 +15,11 @@ auto waybar::modules::Memory::update() -> void { unsigned long memfree; if (meminfo_.count("MemAvailable")) { // New kernels (3.4+) have an accurate available memory field. - memfree = meminfo_["MemAvailable"]; + memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; } else { // Old kernel; give a best-effort approximation of available memory. memfree = meminfo_["MemFree"] + meminfo_["Buffers"] + meminfo_["Cached"] + - meminfo_["SReclaimable"] - meminfo_["Shmem"]; + meminfo_["SReclaimable"] - meminfo_["Shmem"] + meminfo_["zfs_size"]; } if (memtotal > 0 && memfree >= 0) { @@ -28,17 +28,39 @@ auto waybar::modules::Memory::update() -> void { auto used_ram_gigabytes = (memtotal - memfree) / std::pow(1024, 2); auto available_ram_gigabytes = memfree / std::pow(1024, 2); - getState(used_ram_percentage); - label_.set_markup(fmt::format(format_, - used_ram_percentage, - fmt::arg("total", total_ram_gigabytes), - fmt::arg("percentage", used_ram_percentage), - fmt::arg("used", used_ram_gigabytes), - fmt::arg("avail", available_ram_gigabytes))); - if (tooltipEnabled()) { - label_.set_tooltip_text(fmt::format("{:.{}f}Gb used", used_ram_gigabytes, 1)); + auto format = format_; + auto state = getState(used_ram_percentage); + 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}; + label_.set_markup(fmt::format(format, + used_ram_percentage, + fmt::arg("icon", getIcon(used_ram_percentage, icons)), + fmt::arg("total", total_ram_gigabytes), + fmt::arg("percentage", used_ram_percentage), + fmt::arg("used", used_ram_gigabytes), + fmt::arg("avail", available_ram_gigabytes))); + } + + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + auto tooltip_format = config_["tooltip-format"].asString(); + label_.set_tooltip_text(fmt::format(tooltip_format, + used_ram_percentage, + fmt::arg("total", total_ram_gigabytes), + fmt::arg("percentage", used_ram_percentage), + fmt::arg("used", used_ram_gigabytes), + fmt::arg("avail", available_ram_gigabytes))); + } else { + label_.set_tooltip_text(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1)); + } } - event_box_.show(); } else { event_box_.hide(); } diff --git a/src/modules/memory/linux.cpp b/src/modules/memory/linux.cpp index 75f05fe3..34d55c93 100644 --- a/src/modules/memory/linux.cpp +++ b/src/modules/memory/linux.cpp @@ -1,8 +1,29 @@ #include "modules/memory.hpp" +static unsigned zfsArcSize() { + std::ifstream zfs_arc_stats{"/proc/spl/kstat/zfs/arcstats"}; + + if (zfs_arc_stats.is_open()) { + std::string name; + std::string type; + unsigned long data{0}; + + std::string line; + while (std::getline(zfs_arc_stats, line)) { + std::stringstream(line) >> name >> type >> data; + + if (name == "size") { + return data / 1024; // convert to kB + } + } + } + + return 0; +} + void waybar::modules::Memory::parseMeminfo() { const std::string data_dir_ = "/proc/meminfo"; - std::ifstream info(data_dir_); + std::ifstream info(data_dir_); if (!info.is_open()) { throw std::runtime_error("Can't open " + data_dir_); } @@ -17,4 +38,6 @@ void waybar::modules::Memory::parseMeminfo() { int64_t value = std::stol(line.substr(posDelim + 1)); meminfo_[name] = value; } + + meminfo_["zfs_size"] = zfsArcSize(); } diff --git a/src/modules/mpd.cpp b/src/modules/mpd/mpd.cpp similarity index 59% rename from src/modules/mpd.cpp rename to src/modules/mpd/mpd.cpp index af831239..6d272867 100644 --- a/src/modules/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -1,16 +1,23 @@ -#include "modules/mpd.hpp" +#include "modules/mpd/mpd.hpp" #include #include +#include +#include "modules/mpd/state.hpp" +#if defined(MPD_NOINLINE) +namespace waybar::modules { +#include "modules/mpd/state.inl.hpp" +} // namespace waybar::modules +#endif waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) : ALabel(config, "mpd", id, "{album} - {artist} - {title}", 5), module_name_(id.empty() ? "mpd" : "mpd#" + id), server_(nullptr), port_(config_["port"].isUInt() ? config["port"].asUInt() : 0), + password_(config_["password"].empty() ? "" : config_["password"].asString()), timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), connection_(nullptr, &mpd_connection_free), - alternate_connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), song_(nullptr, &mpd_song_free) { if (!config_["port"].isNull() && !config_["port"].isUInt()) { @@ -28,73 +35,33 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) server_ = config["server"].asCString(); } - event_listener().detach(); - event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &MPD::handlePlayPause)); } auto waybar::modules::MPD::update() -> void { - std::lock_guard guard(connection_lock_); - tryConnect(); - - if (connection_ != nullptr) { - try { - bool wasPlaying = playing(); - if(!wasPlaying) { - // Wait until the periodic_updater has stopped - std::lock_guard periodic_guard(periodic_lock_); - } - fetchState(); - if (!wasPlaying && playing()) { - periodic_updater().detach(); - } - } catch (const std::exception& e) { - spdlog::error("{}: {}", module_name_, e.what()); - state_ = MPD_STATE_UNKNOWN; - } - } - - setLabel(); + context_.update(); // Call parent update ALabel::update(); } -std::thread waybar::modules::MPD::event_listener() { - return std::thread([this] { - while (true) { - try { - if (connection_ == nullptr) { - // Retry periodically if no connection - dp.emit(); - std::this_thread::sleep_for(interval_); - } else { - waitForEvent(); - dp.emit(); - } - } catch (const std::exception& e) { - if (strcmp(e.what(), "Connection to MPD closed") == 0) { - spdlog::debug("{}: {}", module_name_, e.what()); - } else { - spdlog::warn("{}: {}", module_name_, e.what()); - } - } +void waybar::modules::MPD::queryMPD() { + if (connection_ != nullptr) { + spdlog::debug("{}: fetching state information", module_name_); + try { + fetchState(); + spdlog::debug("{}: fetch complete", module_name_); + } catch (std::exception const& e) { + spdlog::error("{}: {}", module_name_, e.what()); + state_ = MPD_STATE_UNKNOWN; } - }); + + dp.emit(); + } } -std::thread waybar::modules::MPD::periodic_updater() { - return std::thread([this] { - std::lock_guard guard(periodic_lock_); - while (connection_ != nullptr && playing()) { - dp.emit(); - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - }); -} - -std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) { +std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; const char* tag = mpd_song_get_tag(song_.get(), type, idx); @@ -131,8 +98,9 @@ void waybar::modules::MPD::setLabel() { } auto format = format_; - - std::string artist, album_artist, album, title, date; + Glib::ustring artist, album_artist, album, title; + std::string date; + int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; std::string stateIcon = ""; @@ -148,8 +116,8 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->add_class("playing"); label_.get_style_context()->remove_class("paused"); } else if (paused()) { - format = - config_["format-paused"].isString() ? config_["format-paused"].asString() : config_["format"].asString(); + format = config_["format-paused"].isString() ? config_["format-paused"].asString() + : config_["format"].asString(); label_.get_style_context()->add_class("paused"); label_.get_style_context()->remove_class("playing"); } @@ -161,6 +129,12 @@ void waybar::modules::MPD::setLabel() { album = getTag(MPD_TAG_ALBUM); title = getTag(MPD_TAG_TITLE); date = getTag(MPD_TAG_DATE); + song_pos = mpd_status_get_song_pos(status_.get()); + volume = mpd_status_get_volume(status_.get()); + if (volume < 0) { + volume = 0; + } + queue_length = mpd_status_get_queue_length(status_.get()); elapsedTime = std::chrono::seconds(mpd_status_get_elapsed_time(status_.get())); totalTime = std::chrono::seconds(mpd_status_get_total_time(status_.get())); } @@ -173,43 +147,62 @@ void waybar::modules::MPD::setLabel() { std::string repeatIcon = getOptionIcon("repeat", repeatActivated); bool singleActivated = mpd_status_get_single(status_.get()); std::string singleIcon = getOptionIcon("single", singleActivated); + if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt()); + if (config_["album-artist-len"].isInt()) album_artist = album_artist.substr(0, config_["album-artist-len"].asInt()); + if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt()); + if (config_["title-len"].isInt()) title = title.substr(0,config_["title-len"].asInt()); - // TODO: format can fail - label_.set_markup( - fmt::format(format, - fmt::arg("artist", Glib::Markup::escape_text(artist).raw()), - fmt::arg("albumArtist", Glib::Markup::escape_text(album_artist).raw()), - fmt::arg("album", Glib::Markup::escape_text(album).raw()), - fmt::arg("title", Glib::Markup::escape_text(title).raw()), - fmt::arg("date", Glib::Markup::escape_text(date).raw()), - fmt::arg("elapsedTime", elapsedTime), - fmt::arg("totalTime", totalTime), - fmt::arg("stateIcon", stateIcon), - fmt::arg("consumeIcon", consumeIcon), - fmt::arg("randomIcon", randomIcon), - fmt::arg("repeatIcon", repeatIcon), - fmt::arg("singleIcon", singleIcon))); + try { + label_.set_markup( + fmt::format(format, + fmt::arg("artist", Glib::Markup::escape_text(artist).raw()), + fmt::arg("albumArtist", Glib::Markup::escape_text(album_artist).raw()), + fmt::arg("album", Glib::Markup::escape_text(album).raw()), + fmt::arg("title", Glib::Markup::escape_text(title).raw()), + fmt::arg("date", Glib::Markup::escape_text(date).raw()), + fmt::arg("volume", volume), + fmt::arg("elapsedTime", elapsedTime), + fmt::arg("totalTime", totalTime), + fmt::arg("songPosition", song_pos), + fmt::arg("queueLength", queue_length), + fmt::arg("stateIcon", stateIcon), + fmt::arg("consumeIcon", consumeIcon), + fmt::arg("randomIcon", randomIcon), + fmt::arg("repeatIcon", repeatIcon), + fmt::arg("singleIcon", singleIcon))); + } catch (fmt::format_error const& e) { + spdlog::warn("mpd: format error: {}", e.what()); + } if (tooltipEnabled()) { std::string tooltip_format; tooltip_format = config_["tooltip-format"].isString() ? config_["tooltip-format"].asString() : "MPD (connected)"; - auto tooltip_text = fmt::format(tooltip_format, - fmt::arg("artist", artist), - fmt::arg("albumArtist", album_artist), - fmt::arg("album", album), - fmt::arg("title", title), - fmt::arg("date", date), - fmt::arg("stateIcon", stateIcon), - fmt::arg("consumeIcon", consumeIcon), - fmt::arg("randomIcon", randomIcon), - fmt::arg("repeatIcon", repeatIcon), - fmt::arg("singleIcon", singleIcon)); - label_.set_tooltip_text(tooltip_text); + try { + auto tooltip_text = fmt::format(tooltip_format, + fmt::arg("artist", artist.raw()), + fmt::arg("albumArtist", album_artist.raw()), + fmt::arg("album", album.raw()), + fmt::arg("title", title.raw()), + fmt::arg("date", date), + fmt::arg("volume", volume), + fmt::arg("elapsedTime", elapsedTime), + fmt::arg("totalTime", totalTime), + fmt::arg("songPosition", song_pos), + fmt::arg("queueLength", queue_length), + fmt::arg("stateIcon", stateIcon), + fmt::arg("consumeIcon", consumeIcon), + fmt::arg("randomIcon", randomIcon), + fmt::arg("repeatIcon", repeatIcon), + fmt::arg("singleIcon", singleIcon)); + label_.set_tooltip_text(tooltip_text); + } catch (fmt::format_error const& e) { + spdlog::warn("mpd: format error (tooltip): {}", e.what()); + } } } -std::string waybar::modules::MPD::getStateIcon() { +std::string waybar::modules::MPD::getStateIcon() const { if (!config_["state-icons"].isObject()) { return ""; } @@ -231,7 +224,7 @@ std::string waybar::modules::MPD::getStateIcon() { } } -std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) { +std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) const { if (!config_[optionName + "-icons"].isObject()) { return ""; } @@ -254,25 +247,30 @@ void waybar::modules::MPD::tryConnect() { } connection_ = - unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free); + detail::unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free); - alternate_connection_ = - unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free); - - if (connection_ == nullptr || alternate_connection_ == nullptr) { + if (connection_ == nullptr) { spdlog::error("{}: Failed to connect to MPD", module_name_); connection_.reset(); - alternate_connection_.reset(); return; } try { checkErrors(connection_.get()); spdlog::debug("{}: Connected to MPD", module_name_); + + if (!password_.empty()) { + bool res = mpd_run_password(connection_.get(), password_.c_str()); + if (!res) { + spdlog::error("{}: Wrong MPD password", module_name_); + connection_.reset(); + return; + } + checkErrors(connection_.get()); + } } catch (std::runtime_error& e) { spdlog::error("{}: Failed to connect to MPD: {}", module_name_, e.what()); connection_.reset(); - alternate_connection_.reset(); } } @@ -285,51 +283,34 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) { case MPD_ERROR_CLOSED: mpd_connection_clear_error(conn); connection_.reset(); - alternate_connection_.reset(); state_ = MPD_STATE_UNKNOWN; throw std::runtime_error("Connection to MPD closed"); default: if (conn) { auto error_message = mpd_connection_get_error_message(conn); + std::string error(error_message); mpd_connection_clear_error(conn); - throw std::runtime_error(std::string(error_message)); + throw std::runtime_error(error); } throw std::runtime_error("Invalid connection"); } } void waybar::modules::MPD::fetchState() { + if (connection_ == nullptr) { + spdlog::error("{}: Not connected to MPD", module_name_); + return; + } + auto conn = connection_.get(); - status_ = unique_status(mpd_run_status(conn), &mpd_status_free); + + status_ = detail::unique_status(mpd_run_status(conn), &mpd_status_free); checkErrors(conn); + state_ = mpd_status_get_state(status_.get()); checkErrors(conn); - song_ = unique_song(mpd_run_current_song(conn), &mpd_song_free); - checkErrors(conn); -} - -void waybar::modules::MPD::waitForEvent() { - auto conn = alternate_connection_.get(); - // Wait for a player (play/pause), option (random, shuffle, etc.), or playlist - // change - if (!mpd_send_idle_mask( - conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { - checkErrors(conn); - return; - } - // alternate_idle_ = true; - - // See issue #277: - // https://github.com/Alexays/Waybar/issues/277 - mpd_recv_idle(conn, /* disable_timeout = */ false); - // See issue #281: - // https://github.com/Alexays/Waybar/issues/281 - std::lock_guard guard(connection_lock_); - - checkErrors(conn); - mpd_response_finish(conn); - + song_ = detail::unique_song(mpd_run_current_song(conn), &mpd_song_free); checkErrors(conn); } @@ -339,24 +320,13 @@ bool waybar::modules::MPD::handlePlayPause(GdkEventButton* const& e) { } if (e->button == 1) { - std::lock_guard guard(connection_lock_); - if (stopped()) { - mpd_run_play(connection_.get()); - } else { - mpd_run_toggle_pause(connection_.get()); - } + if (state_ == MPD_STATE_PLAY) + context_.pause(); + else + context_.play(); } else if (e->button == 3) { - std::lock_guard guard(connection_lock_); - mpd_run_stop(connection_.get()); + context_.stop(); } return true; } - -bool waybar::modules::MPD::stopped() { - return connection_ == nullptr || state_ == MPD_STATE_UNKNOWN || state_ == MPD_STATE_STOP; -} - -bool waybar::modules::MPD::playing() { return connection_ != nullptr && state_ == MPD_STATE_PLAY; } - -bool waybar::modules::MPD::paused() { return connection_ != nullptr && state_ == MPD_STATE_PAUSE; } diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp new file mode 100644 index 00000000..ffe18e7c --- /dev/null +++ b/src/modules/mpd/state.cpp @@ -0,0 +1,383 @@ +#include "modules/mpd/state.hpp" + +#include +#include + +#include "modules/mpd/mpd.hpp" +#if defined(MPD_NOINLINE) +namespace waybar::modules { +#include "modules/mpd/state.inl.hpp" +} // namespace waybar::modules +#endif + +namespace waybar::modules::detail { + +#define IDLE_RUN_NOIDLE_AND_CMD(...) \ + if (idle_connection_.connected()) { \ + idle_connection_.disconnect(); \ + auto conn = ctx_->connection().get(); \ + if (!mpd_run_noidle(conn)) { \ + if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ + spdlog::error("mpd: Idle: failed to unregister for IDLE events"); \ + ctx_->checkErrors(conn); \ + } \ + } \ + __VA_ARGS__; \ + } + +void Idle::play() { + IDLE_RUN_NOIDLE_AND_CMD(mpd_run_play(conn)); + + ctx_->setState(std::make_unique(ctx_)); +} + +void Idle::pause() { + IDLE_RUN_NOIDLE_AND_CMD(mpd_run_pause(conn, true)); + + ctx_->setState(std::make_unique(ctx_)); +} + +void Idle::stop() { + IDLE_RUN_NOIDLE_AND_CMD(mpd_run_stop(conn)); + + ctx_->setState(std::make_unique(ctx_)); +} + +#undef IDLE_RUN_NOIDLE_AND_CMD + +void Idle::update() noexcept { + // This is intentionally blank. +} + +void Idle::entry() noexcept { + auto conn = ctx_->connection().get(); + assert(conn != nullptr); + + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Idle: failed to register for IDLE events"); + } else { + spdlog::debug("mpd: Idle: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Idle::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, + mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } +} + +void Idle::exit() noexcept { + if (idle_connection_.connected()) { + idle_connection_.disconnect(); + spdlog::debug("mpd: Idle: unwatching FD"); + } +} + +bool Idle::on_io(Glib::IOCondition const&) { + auto conn = ctx_->connection().get(); + + // callback should do this: + enum mpd_idle events = mpd_recv_idle(conn, /* ignore_timeout?= */ false); + spdlog::debug("mpd: Idle: recv_idle events -> {}", events); + + mpd_response_finish(conn); + try { + ctx_->checkErrors(conn); + } catch (std::exception const& e) { + spdlog::warn("mpd: Idle: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + ctx_->fetchState(); + mpd_state state = ctx_->state(); + + if (state == MPD_STATE_STOP) { + ctx_->emit(); + ctx_->setState(std::make_unique(ctx_)); + } else if (state == MPD_STATE_PLAY) { + ctx_->emit(); + ctx_->setState(std::make_unique(ctx_)); + } else if (state == MPD_STATE_PAUSE) { + ctx_->emit(); + ctx_->setState(std::make_unique(ctx_)); + } else { + ctx_->emit(); + // self transition + ctx_->setState(std::make_unique(ctx_)); + } + + return false; +} + +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); + spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); +} + +void Playing::exit() noexcept { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + spdlog::debug("mpd: Playing: disabled 1 second periodic timer."); + } +} + +bool Playing::on_timer() { + // Attempt to connect with MPD. + try { + ctx_->tryConnect(); + + // Success? + if (!ctx_->is_connected()) { + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + ctx_->fetchState(); + + if (!ctx_->is_playing()) { + if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + } else { + ctx_->setState(std::make_unique(ctx_)); + } + return false; + } + + ctx_->queryMPD(); + ctx_->emit(); + } catch (std::exception const& e) { + spdlog::warn("mpd: Playing: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + return true; +} + +void Playing::stop() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_stop(ctx_->connection().get()); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Playing::pause() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_pause(ctx_->connection().get(), true); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Playing::update() noexcept { ctx_->do_update(); } + +void Paused::entry() noexcept { + sigc::slot timer_slot = sigc::mem_fun(*this, &Paused::on_timer); + timer_connection_ = Glib::signal_timeout().connect(timer_slot, /* milliseconds */ 200); + spdlog::debug("mpd: Paused: enabled 200 ms periodic timer."); +} + +void Paused::exit() noexcept { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + spdlog::debug("mpd: Paused: disabled 200 ms periodic timer."); + } +} + +bool Paused::on_timer() { + bool rc = true; + + // Attempt to connect with MPD. + try { + ctx_->tryConnect(); + + // Success? + if (!ctx_->is_connected()) { + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + ctx_->fetchState(); + + ctx_->emit(); + + if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } else if (ctx_->is_playing()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } else if (ctx_->is_stopped()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Paused: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } + + return rc; +} + +void Paused::play() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_play(ctx_->connection().get()); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Paused::stop() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_stop(ctx_->connection().get()); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Paused::update() noexcept { ctx_->do_update(); } + +void Stopped::entry() noexcept { + sigc::slot timer_slot = sigc::mem_fun(*this, &Stopped::on_timer); + timer_connection_ = Glib::signal_timeout().connect(timer_slot, /* milliseconds */ 200); + spdlog::debug("mpd: Stopped: enabled 200 ms periodic timer."); +} + +void Stopped::exit() noexcept { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + spdlog::debug("mpd: Stopped: disabled 200 ms periodic timer."); + } +} + +bool Stopped::on_timer() { + bool rc = true; + + // Attempt to connect with MPD. + try { + ctx_->tryConnect(); + + // Success? + if (!ctx_->is_connected()) { + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + ctx_->fetchState(); + + ctx_->emit(); + + if (ctx_->is_stopped()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } else if (ctx_->is_playing()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } else if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Stopped: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + rc = false; + } + + return rc; +} + +void Stopped::play() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_play(ctx_->connection().get()); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Stopped::pause() { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + + mpd_run_pause(ctx_->connection().get(), true); + } + + ctx_->setState(std::make_unique(ctx_)); +} + +void Stopped::update() noexcept { ctx_->do_update(); } + +void Disconnected::arm_timer(int interval) noexcept { + // unregister timer, if present + disarm_timer(); + + // register timer + 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."); +} + +void Disconnected::disarm_timer() noexcept { + // unregister timer, if present + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + spdlog::debug("mpd: Disconnected: disabled interval timer."); + } +} + +void Disconnected::entry() noexcept { + ctx_->emit(); + arm_timer(1'000); +} + +void Disconnected::exit() noexcept { + disarm_timer(); +} + +bool Disconnected::on_timer() { + // Attempt to connect with MPD. + try { + ctx_->tryConnect(); + + // Success? + if (ctx_->is_connected()) { + ctx_->fetchState(); + ctx_->emit(); + + if (ctx_->is_playing()) { + ctx_->setState(std::make_unique(ctx_)); + } else if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + } else { + ctx_->setState(std::make_unique(ctx_)); + } + + return false; // do not rearm timer + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Disconnected: error: {}", e.what()); + } + + arm_timer(ctx_->interval() * 1'000); + + return false; +} + +void Disconnected::update() noexcept { ctx_->do_update(); } + +} // namespace waybar::modules::detail diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 0639d3e4..e7b20ab5 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -1,125 +1,134 @@ -#include "modules/network.hpp" +#include #include #include -#include + #include +#include +#include +#include + +#include "modules/network.hpp" #include "util/format.hpp" +#ifdef WANT_RFKILL #include "util/rfkill.hpp" +#endif namespace { - using namespace waybar::util; - -constexpr const char *NETSTAT_FILE = - "/proc/net/netstat"; // std::ifstream does not take std::string_view as param -constexpr std::string_view BANDWIDTH_CATEGORY = "IpExt"; -constexpr std::string_view BANDWIDTH_DOWN_TOTAL_KEY = "InOctets"; -constexpr std::string_view BANDWIDTH_UP_TOTAL_KEY = "OutOctets"; - -std::ifstream netstat(NETSTAT_FILE); -std::optional read_netstat(std::string_view category, std::string_view key) { - if (!netstat) { - spdlog::warn("Failed to open netstat file {}", NETSTAT_FILE); - return {}; - } - netstat.seekg(std::ios_base::beg); - - // finding corresponding line (category) - // looks into the file for the first line starting by the 'category' string - auto starts_with = [](const std::string &str, std::string_view start) { - return start == std::string_view{str.data(), std::min(str.size(), start.size())}; - }; - - std::string read; - while (std::getline(netstat, read) && !starts_with(read, category)) - ; - if (!starts_with(read, category)) { - spdlog::warn("Category '{}' not found in netstat file {}", category, NETSTAT_FILE); - return {}; - } - - // finding corresponding column (key) - // looks into the fetched line for the first word (space separated) equal to 'key' - int index = 0; - auto r_it = read.begin(); - auto k_it = key.begin(); - while (k_it != key.end() && r_it != read.end()) { - if (*r_it != *k_it) { - r_it = std::find(r_it, read.end(), ' '); - if (r_it != read.end()) { - ++r_it; - } - k_it = key.begin(); - ++index; - } else { - ++r_it; - ++k_it; - } - } - - if (r_it == read.end() && k_it != key.end()) { - spdlog::warn( - "Key '{}' not found in category '{}' of netstat file {}", key, category, NETSTAT_FILE); - return {}; - } - - // finally accessing value - // accesses the line right under the fetched one - std::getline(netstat, read); - assert(starts_with(read, category)); - std::istringstream iss(read); - while (index--) { - std::getline(iss, read, ' '); - } - unsigned long long value; - iss >> value; - return value; -} +constexpr const char *DEFAULT_FORMAT = "{ifname}"; } // namespace +constexpr const char *NETDEV_FILE = + "/proc/net/dev"; // std::ifstream does not take std::string_view as param +std::optional> +waybar::modules::Network::readBandwidthUsage() { + std::ifstream netdev(NETDEV_FILE); + if (!netdev) { + spdlog::warn("Failed to open netdev file {}", NETDEV_FILE); + return {}; + } + + std::string line; + // skip the headers (first two lines) + std::getline(netdev, line); + std::getline(netdev, line); + + unsigned long long receivedBytes = 0ull; + unsigned long long transmittedBytes = 0ull; + while (std::getline(netdev, line)) { + std::istringstream iss(line); + + std::string ifacename; + iss >> ifacename; // ifacename contains "eth0:" + ifacename.pop_back(); // remove trailing ':' + if (!checkInterface(ifacename)) { + continue; + } + + // The rest of the line consists of whitespace separated counts divided + // into two groups (receive and transmit). Each group has the following + // columns: bytes, packets, errs, drop, fifo, frame, compressed, multicast + // + // We only care about the bytes count, so we'll just ignore the 7 other + // columns. + unsigned long long r = 0ull; + unsigned long long t = 0ull; + // Read received bytes + iss >> r; + // Skip all the other columns in the received group + for (int colsToSkip = 7; colsToSkip > 0; colsToSkip--) { + // skip whitespace between columns + while (iss.peek() == ' ') { iss.ignore(); } + // skip the irrelevant column + while (iss.peek() != ' ') { iss.ignore(); } + } + // Read transmit bytes + iss >> t; + + receivedBytes += r; + transmittedBytes += t; + } + + return {{receivedBytes, transmittedBytes}}; +} + waybar::modules::Network::Network(const std::string &id, const Json::Value &config) - : ALabel(config, "network", id, "{ifname}", 60), + : ALabel(config, "network", id, DEFAULT_FORMAT, 60), ifid_(-1), family_(config["family"] == "ipv6" ? AF_INET6 : AF_INET), efd_(-1), ev_fd_(-1), - cidr_(-1), + want_route_dump_(false), + want_link_dump_(false), + want_addr_dump_(false), + dump_in_progress_(false), + cidr_(0), signal_strength_dbm_(0), signal_strength_(0), - frequency_(0), - rfkill_{RFKILL_TYPE_WLAN} { - auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY); - auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY); - if (down_octets) { - bandwidth_down_total_ = *down_octets; +#ifdef WANT_RFKILL + rfkill_{RFKILL_TYPE_WLAN}, +#endif + frequency_(0) { + + // Start with some "text" in the module's label_, update() will then + // update it. Since the text should be different, update() will be able + // to show or hide the event_box_. This is to work around the case where + // the module start with no text, but the the event_box_ is shown. + label_.set_markup(""); + + auto bandwidth = readBandwidthUsage(); + if (bandwidth.has_value()) { + bandwidth_down_total_ = (*bandwidth).first; + bandwidth_up_total_ = (*bandwidth).second; } else { bandwidth_down_total_ = 0; + bandwidth_up_total_ = 0; } - if (up_octets) { - bandwidth_up_total_ = *up_octets; + if (!config_["interface"].isString()) { + // "interface" isn't configure, then try to guess the external + // interface currently used for internet. + want_route_dump_ = true; } else { - bandwidth_up_total_ = 0; + // Look for an interface that match "interface" + // and then find the address associated with it. + want_link_dump_ = true; + want_addr_dump_ = true; } createEventSocket(); createInfoSocket(); - auto default_iface = getPreferredIface(-1, false); - if (default_iface != -1) { - ifid_ = default_iface; - char ifname[IF_NAMESIZE]; - if_indextoname(default_iface, ifname); - ifname_ = ifname; - getInterfaceAddress(); - } + dp.emit(); + // Ask for a dump of interfaces and then addresses to populate our + // information. First the interface dump, and once done, the callback + // will be called again which will ask for addresses dump. + askForStateDump(); worker(); } waybar::modules::Network::~Network() { if (ev_fd_ > -1) { - eventfd_write(ev_fd_, 1); - std::this_thread::sleep_for(std::chrono::milliseconds(150)); close(ev_fd_); } if (efd_ > -1) { @@ -145,17 +154,29 @@ void waybar::modules::Network::createEventSocket() { ev_sock_ = nl_socket_alloc(); 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); nl_join_groups(ev_sock_, groups); // Deprecated if (nl_connect(ev_sock_, NETLINK_ROUTE) != 0) { throw std::runtime_error("Can't connect network socket"); } + 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); } + 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); + } + } + efd_ = epoll_create1(EPOLL_CLOEXEC); if (efd_ < 0) { throw std::runtime_error("Can't create epoll"); @@ -192,7 +213,7 @@ void waybar::modules::Network::createInfoSocket() { } nl80211_id_ = genl_ctrl_resolve(sock_, "nl80211"); if (nl80211_id_ < 0) { - throw std::runtime_error("Can't resolve nl80211 interface"); + spdlog::warn("Can't resolve nl80211 interface"); } } @@ -208,23 +229,40 @@ void waybar::modules::Network::worker() { } thread_timer_.sleep_for(interval_); }; - thread_rfkill_ = [this] { - rfkill_.waitForEvent(); - { - std::lock_guard lock(mutex_); - if (ifid_ > 0) { - getInfo(); - dp.emit(); - } - } - }; +#ifdef WANT_RFKILL + rfkill_.on_update.connect([this](auto &) { + /* If we are here, it's likely that the network thread already holds the mutex and will be + * holding it for a next few seconds. + * Let's delegate the update to the timer thread instead of blocking the main thread. + */ + thread_timer_.wake_up(); + }); +#else + spdlog::warn("Waybar has been built without rfkill support."); +#endif thread_ = [this] { std::array events{}; int ec = epoll_wait(efd_, events.data(), EPOLL_MAX, -1); if (ec > 0) { for (auto i = 0; i < ec; i++) { - if (events[i].data.fd != nl_socket_get_fd(ev_sock_) || nl_recvmsgs_default(ev_sock_) < 0) { + if (events[i].data.fd == nl_socket_get_fd(ev_sock_)) { + int rc = 0; + // Read as many message as possible, until the socket blocks + while (true) { + errno = 0; + rc = nl_recvmsgs_default(ev_sock_); + if (rc == -NLE_AGAIN || errno == EAGAIN) { + rc = 0; + break; + } + } + if (rc < 0) { + spdlog::error("nl_recvmsgs_default error: {}", nl_geterror(-rc)); + thread_.stop(); + break; + } + } else { thread_.stop(); break; } @@ -235,10 +273,13 @@ void waybar::modules::Network::worker() { const std::string waybar::modules::Network::getNetworkState() const { if (ifid_ == -1) { +#ifdef WANT_RFKILL if (rfkill_.getState()) return "disabled"; +#endif return "disconnected"; } + if (!carrier_) return "disconnected"; if (ipaddr_.empty()) return "linked"; if (essid_.empty()) return "ethernet"; return "wifi"; @@ -247,20 +288,21 @@ const std::string waybar::modules::Network::getNetworkState() const { auto waybar::modules::Network::update() -> void { std::lock_guard lock(mutex_); std::string tooltip_format; - auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY); - auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY); - unsigned long long bandwidth_down = 0; - if (down_octets) { - bandwidth_down = *down_octets - bandwidth_down_total_; - bandwidth_down_total_ = *down_octets; + auto bandwidth = readBandwidthUsage(); + auto bandwidth_down = 0ull; + auto bandwidth_up = 0ull; + if (bandwidth.has_value()) { + auto down_octets = (*bandwidth).first; + auto up_octets = (*bandwidth).second; + + bandwidth_down = down_octets - bandwidth_down_total_; + bandwidth_down_total_ = down_octets; + + bandwidth_up = up_octets - bandwidth_up_total_; + bandwidth_up_total_ = up_octets; } - unsigned long long bandwidth_up = 0; - if (up_octets) { - bandwidth_up = *up_octets - bandwidth_up_total_; - bandwidth_up_total_ = *up_octets; - } if (!alt_) { auto state = getNetworkState(); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { @@ -268,6 +310,10 @@ auto waybar::modules::Network::update() -> void { } if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + default_format_ = config_["format"].asString(); + } else { + default_format_ = DEFAULT_FORMAT; } if (config_["tooltip-format-" + state].isString()) { tooltip_format = config_["tooltip-format-" + state].asString(); @@ -288,6 +334,7 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), + fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("frequency", frequency_), fmt::arg("icon", getIcon(signal_strength_, state_)), @@ -316,6 +363,7 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), + fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("frequency", frequency_), fmt::arg("icon", getIcon(signal_strength_, state_)), @@ -324,7 +372,7 @@ auto waybar::modules::Network::update() -> void { fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")), fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s"))); - if (label_.get_tooltip_text() != text) { + if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_text(tooltip_text); } } else if (label_.get_tooltip_text() != text) { @@ -336,349 +384,350 @@ auto waybar::modules::Network::update() -> void { ALabel::update(); } -// Based on https://gist.github.com/Yawning/c70d804d4b8ae78cc698 -int waybar::modules::Network::getExternalInterface(int skip_idx) const { - static const uint32_t route_buffer_size = 8192; - struct nlmsghdr * hdr = nullptr; - struct rtmsg * rt = nullptr; - char resp[route_buffer_size] = {0}; - int ifidx = -1; - - /* Prepare request. */ - constexpr uint32_t reqlen = NLMSG_SPACE(sizeof(*rt)); - char req[reqlen] = {0}; - - /* Build the RTM_GETROUTE request. */ - hdr = reinterpret_cast(req); - hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*rt)); - hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; - hdr->nlmsg_type = RTM_GETROUTE; - rt = static_cast(NLMSG_DATA(hdr)); - rt->rtm_family = family_; - rt->rtm_table = RT_TABLE_MAIN; - - /* Issue the query. */ - if (netlinkRequest(req, reqlen) < 0) { - goto out; - } - - /* Read the response(s). - * - * WARNING: All the packets generated by the request must be consumed (as in, - * consume responses till NLMSG_DONE/NLMSG_ERROR is encountered). - */ - do { - auto len = netlinkResponse(resp, route_buffer_size); - if (len < 0) { - goto out; - } - - /* Parse the response payload into netlink messages. */ - for (hdr = reinterpret_cast(resp); NLMSG_OK(hdr, len); - hdr = NLMSG_NEXT(hdr, len)) { - if (hdr->nlmsg_type == NLMSG_DONE) { - goto out; - } - if (hdr->nlmsg_type == NLMSG_ERROR) { - /* Even if we found the interface index, something is broken with the - * netlink socket, so return an error. - */ - ifidx = -1; - goto out; - } - - /* If we found the correct answer, skip parsing the attributes. */ - if (ifidx != -1) { - continue; - } - - /* Find the message(s) concerting the main routing table, each message - * corresponds to a single routing table entry. - */ - rt = static_cast(NLMSG_DATA(hdr)); - if (rt->rtm_table != RT_TABLE_MAIN) { - continue; - } - - /* Parse all the attributes for a single routing table entry. */ - struct rtattr *attr = RTM_RTA(rt); - uint64_t attrlen = RTM_PAYLOAD(hdr); - bool has_gateway = false; - bool has_destination = false; - int temp_idx = -1; - for (; RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) { - /* Determine if this routing table entry corresponds to the default - * route by seeing if it has a gateway, and if a destination addr is - * set, that it is all 0s. - */ - switch (attr->rta_type) { - case RTA_GATEWAY: - /* The gateway of the route. - * - * If someone every needs to figure out the gateway address as well, - * it's here as the attribute payload. - */ - 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 = (family_ == AF_INET) ? 4 : 16; - unsigned char c = 0; - size_t dstlen = RTA_PAYLOAD(attr); - if (dstlen != nr_zeroes) { - break; - } - for (uint32_t i = 0; i < dstlen; i += 1) { - c |= *((unsigned char *)RTA_DATA(attr) + i); - } - has_destination = (c == 0); - break; - } - case RTA_OIF: - /* The output interface index. */ - temp_idx = *static_cast(RTA_DATA(attr)); - break; - default: - break; - } - } - /* If this is the default route, and we know the interface index, - * we can stop parsing this message. - */ - if (has_gateway && !has_destination && temp_idx != -1 && temp_idx != skip_idx) { - ifidx = temp_idx; - break; - } - } - } while (true); - -out: - return ifidx; -} - -void waybar::modules::Network::getInterfaceAddress() { - struct ifaddrs *ifaddr, *ifa; - cidr_ = 0; - int success = getifaddrs(&ifaddr); - if (success != 0) { - return; - } - ifa = ifaddr; - while (ifa != nullptr) { - if (ifa->ifa_addr != nullptr && ifa->ifa_addr->sa_family == family_ && - ifa->ifa_name == ifname_) { - char ipaddr[INET6_ADDRSTRLEN]; - char netmask[INET6_ADDRSTRLEN]; - unsigned int cidr = 0; - if (family_ == AF_INET) { - ipaddr_ = inet_ntop(AF_INET, - &reinterpret_cast(ifa->ifa_addr)->sin_addr, - ipaddr, - INET_ADDRSTRLEN); - auto net_addr = reinterpret_cast(ifa->ifa_netmask); - netmask_ = inet_ntop(AF_INET, &net_addr->sin_addr, netmask, INET_ADDRSTRLEN); - unsigned int cidrRaw = net_addr->sin_addr.s_addr; - while (cidrRaw) { - cidr += cidrRaw & 1; - cidrRaw >>= 1; - } - } else { - ipaddr_ = inet_ntop(AF_INET6, - &reinterpret_cast(ifa->ifa_addr)->sin6_addr, - ipaddr, - INET6_ADDRSTRLEN); - auto net_addr = reinterpret_cast(ifa->ifa_netmask); - netmask_ = inet_ntop(AF_INET6, &net_addr->sin6_addr, netmask, INET6_ADDRSTRLEN); - for (size_t i = 0; i < sizeof(net_addr->sin6_addr.s6_addr); ++i) { - unsigned char cidrRaw = net_addr->sin6_addr.s6_addr[i]; - while (cidrRaw) { - cidr += cidrRaw & 1; - cidrRaw >>= 1; - } - } - } - cidr_ = cidr; - break; - } - ifa = ifa->ifa_next; - } - freeifaddrs(ifaddr); -} - -int waybar::modules::Network::netlinkRequest(void *req, uint32_t reqlen, uint32_t groups) const { - struct sockaddr_nl sa = {}; - sa.nl_family = AF_NETLINK; - sa.nl_groups = groups; - struct iovec iov = {req, reqlen}; - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - return sendmsg(nl_socket_get_fd(ev_sock_), &msg, 0); -} - -int waybar::modules::Network::netlinkResponse(void *resp, uint32_t resplen, uint32_t groups) const { - struct sockaddr_nl sa = {}; - sa.nl_family = AF_NETLINK; - sa.nl_groups = groups; - struct iovec iov = {resp, resplen}; - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - auto ret = recvmsg(nl_socket_get_fd(ev_sock_), &msg, 0); - if (msg.msg_flags & MSG_TRUNC) { - return -1; - } - return ret; -} - -bool waybar::modules::Network::checkInterface(struct ifinfomsg *rtif, std::string name) { +bool waybar::modules::Network::checkInterface(std::string name) { if (config_["interface"].isString()) { return config_["interface"].asString() == name || wildcardMatch(config_["interface"].asString(), name); } - // getExternalInterface may need some delay to detect external interface - for (uint8_t tries = 0; tries < MAX_RETRY; tries += 1) { - auto external_iface = getExternalInterface(); - if (external_iface > 0) { - return external_iface == rtif->ifi_index; - } - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } return false; } -int waybar::modules::Network::getPreferredIface(int skip_idx, bool wait) const { - int ifid = -1; - if (config_["interface"].isString()) { - ifid = if_nametoindex(config_["interface"].asCString()); - if (ifid > 0) { - return ifid; - } else { - // Try with wildcard - struct ifaddrs *ifaddr, *ifa; - int success = getifaddrs(&ifaddr); - if (success != 0) { - return -1; - } - ifa = ifaddr; - ifid = -1; - while (ifa != nullptr) { - if (wildcardMatch(config_["interface"].asString(), ifa->ifa_name)) { - ifid = if_nametoindex(ifa->ifa_name); - break; - } - ifa = ifa->ifa_next; - } - freeifaddrs(ifaddr); - return ifid; - } - } - // getExternalInterface may need some delay to detect external interface - for (uint8_t tries = 0; tries < MAX_RETRY; tries += 1) { - ifid = getExternalInterface(skip_idx); - if (ifid > 0) { - return ifid; - } - if (wait) { - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - } - return -1; -} - void waybar::modules::Network::clearIface() { + ifid_ = -1; + ifname_.clear(); essid_.clear(); ipaddr_.clear(); + gwaddr_.clear(); netmask_.clear(); + carrier_ = false; cidr_ = 0; signal_strength_dbm_ = 0; signal_strength_ = 0; frequency_ = 0; } -void waybar::modules::Network::checkNewInterface(struct ifinfomsg *rtif) { - auto new_iface = getPreferredIface(rtif->ifi_index); - if (new_iface != -1) { - ifid_ = new_iface; - char ifname[IF_NAMESIZE]; - if_indextoname(new_iface, ifname); - ifname_ = ifname; - getInterfaceAddress(); - thread_timer_.wake_up(); - } else { - ifid_ = -1; - dp.emit(); - } -} - int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { auto net = static_cast(data); std::lock_guard lock(net->mutex_); auto nh = nlmsg_hdr(msg); - auto ifi = static_cast(NLMSG_DATA(nh)); - if (nh->nlmsg_type == RTM_DELADDR) { - // Check for valid interface - if (ifi->ifi_index == net->ifid_) { - net->ipaddr_.clear(); - net->netmask_.clear(); - net->cidr_ = 0; - if (!(ifi->ifi_flags & IFF_RUNNING)) { - net->clearIface(); - // Check for a new interface and get info - net->checkNewInterface(ifi); - } else { - net->dp.emit(); - } + bool is_del_event = false; + + switch (nh->nlmsg_type) { + case RTM_DELLINK: + is_del_event = true; + case RTM_NEWLINK: { + struct ifinfomsg *ifi = static_cast(NLMSG_DATA(nh)); + ssize_t attrlen = IFLA_PAYLOAD(nh); + struct rtattr *ifla = IFLA_RTA(ifi); + const char *ifname = NULL; + size_t ifname_len = 0; + std::optional carrier; + + if (net->ifid_ != -1 && ifi->ifi_index != net->ifid_) { return NL_OK; } - } else if (nh->nlmsg_type == RTM_NEWLINK || nh->nlmsg_type == RTM_DELLINK) { - char ifname[IF_NAMESIZE]; - if_indextoname(ifi->ifi_index, ifname); - // Check for valid interface - if (ifi->ifi_index != net->ifid_ && net->checkInterface(ifi, ifname)) { - net->ifname_ = ifname; - net->ifid_ = ifi->ifi_index; - // Get Iface and WIFI info - net->getInterfaceAddress(); - net->thread_timer_.wake_up(); - return NL_OK; - } else if (ifi->ifi_index == net->ifid_ && - (!(ifi->ifi_flags & IFF_RUNNING) || !(ifi->ifi_flags & IFF_UP) || - !net->checkInterface(ifi, ifname))) { + + // Check if the interface goes "down" and if we want to detect the + // external interface. + if (net->ifid_ != -1 && !(ifi->ifi_flags & IFF_UP) + && !net->config_["interface"].isString()) { + // The current interface is now down, all the routes associated with + // it have been deleted, so start looking for a new default route. + spdlog::debug("network: if{} down", net->ifid_); net->clearIface(); - // Check for a new interface and get info - net->checkNewInterface(ifi); + net->dp.emit(); + net->want_route_dump_ = true; + net->askForStateDump(); return NL_OK; } - } else { - char ifname[IF_NAMESIZE]; - if_indextoname(ifi->ifi_index, ifname); - // Auto detected network can also be assigned here - if (ifi->ifi_index != net->ifid_ && net->checkInterface(ifi, ifname)) { - // If iface is different, clear data - if (ifi->ifi_index != net->ifid_) { - net->clearIface(); + + for (; RTA_OK(ifla, attrlen); ifla = RTA_NEXT(ifla, attrlen)) { + switch (ifla->rta_type) { + case IFLA_IFNAME: + ifname = static_cast(RTA_DATA(ifla)); + ifname_len = RTA_PAYLOAD(ifla) - 1; // minus \0 + break; + case IFLA_CARRIER: { + carrier = *(char*)RTA_DATA(ifla) == 1; + break; + } } - net->ifname_ = ifname; - net->ifid_ = ifi->ifi_index; } - // Check for valid interface - if (ifi->ifi_index == net->ifid_) { - // Get Iface and WIFI info - net->getInterfaceAddress(); - net->thread_timer_.wake_up(); + + if (!is_del_event && ifi->ifi_index == net->ifid_) { + // Update interface information + if (net->ifname_.empty() && ifname != NULL) { + std::string new_ifname (ifname, ifname_len); + net->ifname_ = new_ifname; + } + if (carrier.has_value()) { + if (net->carrier_ != *carrier) { + if (*carrier) { + // Ask for WiFi information + net->thread_timer_.wake_up(); + } else { + // clear state related to WiFi connection + net->essid_.clear(); + net->signal_strength_dbm_ = 0; + net->signal_strength_ = 0; + net->frequency_ = 0; + } + } + net->carrier_ = carrier.value(); + } + } else if (!is_del_event && net->ifid_ == -1) { + // Checking if it's an interface we care about. + std::string new_ifname (ifname, ifname_len); + if (net->checkInterface(new_ifname)) { + spdlog::debug("network: selecting new interface {}/{}", new_ifname, ifi->ifi_index); + + net->ifname_ = new_ifname; + net->ifid_ = ifi->ifi_index; + if (carrier.has_value()) { + net->carrier_ = carrier.value(); + } + net->thread_timer_.wake_up(); + /* An address for this new interface should be received via an + * RTM_NEWADDR event either because we ask for a dump of both links + * and addrs, or because this interface has just been created and + * the addr will be sent after the RTM_NEWLINK event. + * So we don't need to do anything. */ + } + } else if (is_del_event && net->ifid_ >= 0) { + // Our interface has been deleted, start looking/waiting for one we care. + spdlog::debug("network: interface {}/{} deleted", net->ifname_, net->ifid_); + + net->clearIface(); + net->dp.emit(); + } + break; + } + + case RTM_DELADDR: + is_del_event = true; + case RTM_NEWADDR: { + struct ifaddrmsg *ifa = static_cast(NLMSG_DATA(nh)); + ssize_t attrlen = IFA_PAYLOAD(nh); + struct rtattr *ifa_rta = IFA_RTA(ifa); + + if ((int)ifa->ifa_index != net->ifid_) { 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: { + 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; + switch (ifa->ifa_family) { + case AF_INET: { + struct in_addr netmask; + netmask.s_addr = htonl(~0 << (32 - ifa->ifa_prefixlen)); + net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, + ipaddr, sizeof (ipaddr)); + } + case AF_INET6: { + struct in6_addr netmask; + 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; + } + net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, + ipaddr, sizeof (ipaddr)); + } + } + spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_); + } else { + net->ipaddr_.clear(); + net->cidr_ = 0; + net->netmask_.clear(); + spdlog::debug("network: {} addr deleted {}/{}", + net->ifname_, + inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), + ipaddr, sizeof (ipaddr)), + ifa->ifa_prefixlen); + } + net->dp.emit(); + break; + } + } + } + break; } - return NL_SKIP; + + char temp_gw_addr[INET6_ADDRSTRLEN]; + case RTM_DELROUTE: + is_del_event = true; + case RTM_NEWROUTE: { + // Based on https://gist.github.com/Yawning/c70d804d4b8ae78cc698 + // to find the interface used to reach the outside world + + struct rtmsg *rtm = static_cast(NLMSG_DATA(nh)); + ssize_t attrlen = RTM_PAYLOAD(nh); + struct rtattr *attr = RTM_RTA(rtm); + bool has_gateway = false; + bool has_destination = false; + int temp_idx = -1; + uint32_t priority = 0; + + + /* Find the message(s) concerting the main routing table, each message + * corresponds to a single routing table entry. + */ + if (rtm->rtm_table != RT_TABLE_MAIN) { + return NL_OK; + } + + /* Parse all the attributes for a single routing table entry. */ + for (; RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) { + /* Determine if this routing table entry corresponds to the default + * route by seeing if it has a gateway, and if a destination addr is + * set, that it is all 0s. + */ + switch(attr->rta_type) { + case RTA_GATEWAY: + /* The gateway of the route. + * + * 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)); + 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; + unsigned char c = 0; + size_t dstlen = RTA_PAYLOAD(attr); + if (dstlen != nr_zeroes) { + break; + } + for (uint32_t i = 0; i < dstlen; i += 1) { + c |= *((unsigned char *)RTA_DATA(attr) + i); + } + has_destination = (c == 0); + break; + } + case RTA_OIF: + /* The output interface index. */ + temp_idx = *static_cast(RTA_DATA(attr)); + break; + case RTA_PRIORITY: + priority = *(uint32_t*)RTA_DATA(attr); + break; + default: + break; + } + } + + // Check if we have a default route. + if (has_gateway && !has_destination && temp_idx != -1) { + // Check if this is the first default route we see, or if this new + // route have a higher priority. + 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(); + net->ifid_ = temp_idx; + net->route_priority = priority; + net->gwaddr_ = temp_gw_addr; + spdlog::debug("network: new default route via {} on if{} metric {}", temp_gw_addr, temp_idx, priority); + + /* Ask ifname associated with temp_idx as well as carrier status */ + struct ifinfomsg ifinfo_hdr = { + .ifi_family = AF_UNSPEC, + .ifi_index = temp_idx, + }; + int err; + err = nl_send_simple(net->ev_sock_, RTM_GETLINK, NLM_F_REQUEST, + &ifinfo_hdr, sizeof (ifinfo_hdr)); + if (err < 0) { + spdlog::error("network: failed to ask link info: {}", err); + /* Ask for a dump of all links instead */ + net->want_link_dump_ = true; + } + + /* Also ask for the address. Asking for a addresses of a specific + * interface doesn't seems to work so ask for a dump of all + * addresses. */ + net->want_addr_dump_ = true; + net->askForStateDump(); + net->thread_timer_.wake_up(); + } else if (is_del_event && temp_idx == net->ifid_ + && net->route_priority == priority) { + spdlog::debug("network: default route deleted {}/if{} metric {}", + net->ifname_, temp_idx, priority); + + net->clearIface(); + net->dp.emit(); + /* Ask for a dump of all routes in case another one is already + * setup. If there's none, there'll be an event with new one + * later. */ + net->want_route_dump_ = true; + net->askForStateDump(); + } + } + break; + } + } + + return NL_OK; +} + +void waybar::modules::Network::askForStateDump(void) { + /* We need to wait until the current dump is done before sending new + * messages. handleEventsDone() is called when a dump is done. */ + if (dump_in_progress_) + return; + + struct rtgenmsg rt_hdr = { + .rtgen_family = AF_UNSPEC, + }; + + 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; + + } else if (want_link_dump_) { + nl_send_simple(ev_sock_, RTM_GETLINK, NLM_F_DUMP, + &rt_hdr, sizeof (rt_hdr)); + want_link_dump_ = false; + 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; + } +} + +int waybar::modules::Network::handleEventsDone(struct nl_msg *msg, void *data) { + auto net = static_cast(data); + net->dump_in_progress_ = false; + net->askForStateDump(); + return NL_OK; } int waybar::modules::Network::handleScan(struct nl_msg *msg, void *data) { diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 571a78e5..cf427800 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -151,8 +151,24 @@ void waybar::modules::Pulseaudio::sourceInfoCb(pa_context * /*context*/, const p */ 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 (i != nullptr && pa->default_sink_name_ == i->name) { + 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; @@ -175,11 +191,11 @@ void waybar::modules::Pulseaudio::sinkInfoCb(pa_context * /*context*/, const pa_ void waybar::modules::Pulseaudio::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) { auto pa = static_cast(data); - pa->default_sink_name_ = i->default_sink_name; + pa->current_sink_name_ = i->default_sink_name; pa->default_source_name_ = i->default_source_name; - pa_context_get_sink_info_by_name(context, i->default_sink_name, sinkInfoCb, data); - pa_context_get_source_info_by_name(context, i->default_source_name, sourceInfoCb, data); + pa_context_get_sink_info_list(context, sinkInfoCb, data); + pa_context_get_source_info_list(context, sourceInfoCb, data); } static const std::array ports = { @@ -194,22 +210,26 @@ static const std::array ports = { "phone", }; -const std::string waybar::modules::Pulseaudio::getPortIcon() const { +const std::vector waybar::modules::Pulseaudio::getPulseIcon() const { + std::vector res = {default_source_name_}; std::string nameLC = port_name_ + form_factor_; std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { if (nameLC.find(port) != std::string::npos) { - return port; + res.push_back(port); + return res; } } - return port_name_; + return res; } auto waybar::modules::Pulseaudio::update() -> void { auto format = format_; + std::string tooltip_format; if (!alt_) { std::string format_name = "format"; - if (monitor_.find("a2dp_sink") != std::string::npos) { + if (monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio + monitor_.find("a2dp-sink") != std::string::npos) { // PipeWire format_name = format_name + "-bluetooth"; label_.get_style_context()->add_class("bluetooth"); } else { @@ -222,28 +242,53 @@ auto waybar::modules::Pulseaudio::update() -> void { } format_name = format_name + "-muted"; label_.get_style_context()->add_class("muted"); + label_.get_style_context()->add_class("sink-muted"); } else { label_.get_style_context()->remove_class("muted"); + label_.get_style_context()->remove_class("sink-muted"); } format = config_[format_name].isString() ? config_[format_name].asString() : format; } // TODO: find a better way to split source/sink std::string format_source = "{volume}%"; - if (source_muted_ && config_["format-source-muted"].isString()) { - format_source = config_["format-source-muted"].asString(); - } else if (!source_muted_ && config_["format-source"].isString()) { - format_source = config_["format-source"].asString(); + if (source_muted_) { + 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()) { + format_source = config_["format-source"].asString(); + } } format_source = fmt::format(format_source, fmt::arg("volume", source_volume_)); label_.set_markup(fmt::format(format, fmt::arg("desc", desc_), fmt::arg("volume", volume_), fmt::arg("format_source", format_source), - fmt::arg("icon", getIcon(volume_, getPortIcon())))); + fmt::arg("source_volume", source_volume_), + fmt::arg("source_desc", source_desc_), + fmt::arg("icon", getIcon(volume_, getPulseIcon())))); getState(volume_); + if (tooltipEnabled()) { - label_.set_tooltip_text(desc_); + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_text(fmt::format( + 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())))); + } else { + label_.set_tooltip_text(desc_); + } } // Call parent update diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp new file mode 100644 index 00000000..87655beb --- /dev/null +++ b/src/modules/river/tags.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include + +#include + +#include "client.hpp" +#include "modules/river/tags.hpp" +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace waybar::modules::river { + +static void listen_focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + uint32_t tags) { + static_cast(data)->handle_focused_tags(tags); +} + +static void listen_view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + struct wl_array *tags) { + static_cast(data)->handle_view_tags(tags); +} + +static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + uint32_t tags) { + static_cast(data)->handle_urgent_tags(tags); +} + +static const zriver_output_status_v1_listener output_status_listener_impl{ + .focused_tags = listen_focused_tags, + .view_tags = listen_view_tags, + .urgent_tags = listen_urgent_tags, +}; + +static void listen_command_success(void *data, + struct zriver_command_callback_v1 *zriver_command_callback_v1, + const char *output) { + // Do nothing but keep listener to avoid crashing when command was successful +} + +static void listen_command_failure(void *data, + struct zriver_command_callback_v1 *zriver_command_callback_v1, + const char *output) { + spdlog::error("failure when selecting/toggling tags {}", output); +} + +static const zriver_command_callback_v1_listener command_callback_listener_impl { + .success = listen_command_success, + .failure = listen_command_failure, +}; + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { + version = std::min(version, 2); + if (version < ZRIVER_OUTPUT_STATUS_V1_URGENT_TAGS_SINCE_VERSION) { + spdlog::warn("river server does not support urgent tags"); + } + static_cast(data)->status_manager_ = static_cast( + wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version)); + } + + if (std::strcmp(interface, zriver_control_v1_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->control_ = static_cast( + wl_registry_bind(registry, name, &zriver_control_v1_interface, version)); + } + + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + /* Ignore event */ +} + + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config) + : waybar::AModule(config, "tags", id, false, false), + status_manager_{nullptr}, + control_{nullptr}, + seat_{nullptr}, + bar_(bar), + box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + output_status_{nullptr} { + struct wl_display * display = Client::inst()->wl_display; + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener_impl, this); + wl_display_roundtrip(display); + + if (!status_manager_) { + spdlog::error("river_status_manager_v1 not advertised"); + return; + } + + if (!control_) { + spdlog::error("river_control_v1 not advertised"); + } + + if (!seat_) { + spdlog::error("wl_seat not advertised"); + } + + box_.set_name("tags"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + // Default to 9 tags, cap at 32 + const uint32_t num_tags = + config["num-tags"].isUInt() ? std::min(32, config_["num-tags"].asUInt()) : 9; + + std::vector tag_labels(num_tags); + for (uint32_t tag = 0; tag < num_tags; ++tag) { + tag_labels[tag] = std::to_string(tag+1); + } + const Json::Value custom_labels = config["tag-labels"]; + if (custom_labels.isArray() && !custom_labels.empty()) { + for (uint32_t tag = 0; tag < std::min(num_tags, custom_labels.size()); ++tag) { + tag_labels[tag] = custom_labels[tag].asString(); + } + } + + uint32_t i = 1; + for (const auto &tag_label : tag_labels) { + Gtk::Button &button = buttons_.emplace_back(tag_label); + button.set_relief(Gtk::RELIEF_NONE); + box_.pack_start(button, false, false, 0); + if (!config_["disable-click"].asBool()) { + button.signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i)); + button.signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i)); + } + button.show(); + i <<= 1; + } + + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); + zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); + + zriver_status_manager_v1_destroy(status_manager_); +} + +Tags::~Tags() { + if (output_status_) { + zriver_output_status_v1_destroy(output_status_); + } + + if (control_) { + zriver_control_v1_destroy(control_); + } +} + +void Tags::handle_primary_clicked(uint32_t tag) { + // Send river command to select tag on left mouse click + zriver_command_callback_v1 *callback; + zriver_control_v1_add_argument(control_, "set-focused-tags"); + zriver_control_v1_add_argument(control_, std::to_string(tag).c_str()); + callback = zriver_control_v1_run_command(control_, seat_); + zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr); +} + +bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { + if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { + // Send river command to toggle tag on right mouse click + zriver_command_callback_v1 *callback; + zriver_control_v1_add_argument(control_, "toggle-focused-tags"); + zriver_control_v1_add_argument(control_, std::to_string(tag).c_str()); + callback = zriver_control_v1_run_command(control_, seat_); + zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr); + } + return true; +} + +void Tags::handle_focused_tags(uint32_t tags) { + uint32_t i = 0; + for (auto &button : buttons_) { + if ((1 << i) & tags) { + button.get_style_context()->add_class("focused"); + } else { + button.get_style_context()->remove_class("focused"); + } + ++i; + } +} + +void Tags::handle_view_tags(struct wl_array *view_tags) { + // First clear all occupied state + for (auto &button : buttons_) { + button.get_style_context()->remove_class("occupied"); + } + + // Set tags with a view to occupied + uint32_t *start = static_cast(view_tags->data); + for (uint32_t *tags = start; tags < start + view_tags->size / sizeof(uint32_t); ++tags) { + uint32_t i = 0; + for (auto &button : buttons_) { + if (*tags & (1 << i)) { + button.get_style_context()->add_class("occupied"); + } + ++i; + } + } +} + +void Tags::handle_urgent_tags(uint32_t tags) { + uint32_t i = 0; + for (auto &button : buttons_) { + if ((1 << i) & tags) { + button.get_style_context()->add_class("urgent"); + } else { + button.get_style_context()->remove_class("urgent"); + } + ++i; + } +} + +} /* namespace waybar::modules::river */ diff --git a/src/modules/simpleclock.cpp b/src/modules/simpleclock.cpp new file mode 100644 index 00000000..3004fc2c --- /dev/null +++ b/src/modules/simpleclock.cpp @@ -0,0 +1,33 @@ +#include "modules/simpleclock.hpp" +#include + +waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) + : ALabel(config, "clock", id, "{:%H:%M}", 60) { + thread_ = [this] { + dp.emit(); + auto now = std::chrono::system_clock::now(); + auto timeout = std::chrono::floor(now + interval_); + auto diff = std::chrono::seconds(timeout.time_since_epoch().count() % interval_.count()); + thread_.sleep_until(timeout - diff); + }; +} + +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); + 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); + label_.set_tooltip_text(tooltip_text); + } else { + label_.set_tooltip_text(text); + } + } + // Call parent update + ALabel::update(); +} diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp new file mode 100644 index 00000000..34c46bdd --- /dev/null +++ b/src/modules/sndio.cpp @@ -0,0 +1,201 @@ +#include "modules/sndio.hpp" +#include +#include +#include +#include +#include + +namespace waybar::modules { + +void ondesc(void *arg, struct sioctl_desc *d, int curval) { + auto self = static_cast(arg); + if (d == NULL) { + // d is NULL when the list is done + return; + } + self->set_desc(d, curval); +} + +void onval(void *arg, unsigned int addr, unsigned int val) { + auto self = static_cast(arg); + self->put_val(addr, val); +} + +auto Sndio::connect_to_sndio() -> void { + hdl_ = sioctl_open(SIO_DEVANY, SIOCTL_READ | SIOCTL_WRITE, 0); + if (hdl_ == nullptr) { + throw std::runtime_error("sioctl_open() failed."); + } + + if (sioctl_ondesc(hdl_, ondesc, this) == 0) { + throw std::runtime_error("sioctl_ondesc() failed."); + } + + if (sioctl_onval(hdl_, onval, this) == 0) { + throw std::runtime_error("sioctl_onval() failed."); + } + + pfds_.reserve(sioctl_nfds(hdl_)); +} + +Sndio::Sndio(const std::string &id, const Json::Value &config) + : ALabel(config, "sndio", id, "{volume}%", 1), + hdl_(nullptr), + pfds_(0), + addr_(0), + volume_(0), + old_volume_(0), + maxval_(0), + muted_(false) { + connect_to_sndio(); + + event_box_.show(); + + event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK | Gdk::BUTTON_PRESS_MASK); + event_box_.signal_scroll_event().connect( + sigc::mem_fun(*this, &Sndio::handleScroll)); + event_box_.signal_button_press_event().connect( + sigc::mem_fun(*this, &Sndio::handleToggle)); + + thread_ = [this] { + dp.emit(); + + int nfds = sioctl_pollfd(hdl_, pfds_.data(), POLLIN); + if (nfds == 0) { + throw std::runtime_error("sioctl_pollfd() failed."); + } + while (poll(pfds_.data(), nfds, -1) < 0) { + if (errno != EINTR) { + throw std::runtime_error("poll() failed."); + } + } + + int revents = sioctl_revents(hdl_, pfds_.data()); + if (revents & POLLHUP) { + spdlog::warn("sndio disconnected!"); + sioctl_close(hdl_); + hdl_ = nullptr; + + // reconnection loop + while (thread_.isRunning()) { + try { + connect_to_sndio(); + } catch(std::runtime_error const& e) { + // avoid leaking hdl_ + if (hdl_) { + sioctl_close(hdl_); + hdl_ = nullptr; + } + // rate limiting for the retries + thread_.sleep_for(interval_); + continue; + } + + spdlog::warn("sndio reconnected!"); + break; + } + } + }; +} + +Sndio::~Sndio() { + sioctl_close(hdl_); +} + +auto Sndio::update() -> void { + auto format = format_; + unsigned int vol = 100. * static_cast(volume_) / static_cast(maxval_); + + if (volume_ == 0) { + label_.get_style_context()->add_class("muted"); + } else { + label_.get_style_context()->remove_class("muted"); + } + + label_.set_markup(fmt::format(format, + fmt::arg("volume", vol), + fmt::arg("raw_value", volume_))); + + ALabel::update(); +} + +auto Sndio::set_desc(struct sioctl_desc *d, unsigned int val) -> void { + std::string name{d->func}; + std::string node_name{d->node0.name}; + + if (name == "level" && node_name == "output" && d->type == SIOCTL_NUM) { + // store addr for output.level value, used in put_val + addr_ = d->addr; + maxval_ = d->maxval; + volume_ = val; + } +} + +auto Sndio::put_val(unsigned int addr, unsigned int val) -> void { + if (addr == addr_) { + volume_ = val; + } +} + +bool Sndio::handleScroll(GdkEventScroll *e) { + // change the volume only when no user provided + // events are configured + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + + // only try to talk to sndio if connected + if (hdl_ == nullptr) return true; + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + int step = 5; + if (config_["scroll-step"].isInt()) { + step = config_["scroll-step"].asInt(); + } + + int new_volume = volume_; + if (muted_) { + new_volume = old_volume_; + } + + if (dir == SCROLL_DIR::UP) { + new_volume += step; + } else if (dir == SCROLL_DIR::DOWN) { + new_volume -= step; + } + new_volume = std::clamp(new_volume, 0, static_cast(maxval_)); + + // quits muted mode if volume changes + muted_ = false; + + sioctl_setval(hdl_, addr_, new_volume); + + return true; +} + +bool Sndio::handleToggle(GdkEventButton* const& e) { + // toggle mute only when no user provided events are configured + if (config_["on-click"].isString()) { + return AModule::handleToggle(e); + } + + // only try to talk to sndio if connected + if (hdl_ == nullptr) return true; + + muted_ = !muted_; + if (muted_) { + // store old volume to be able to restore it later + old_volume_ = volume_; + sioctl_setval(hdl_, addr_, 0); + } else { + sioctl_setval(hdl_, addr_, old_volume_); + } + + return true; +} + +} /* namespace waybar::modules */ diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 868fcd6e..414f1515 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -4,7 +4,7 @@ namespace waybar::modules::SNI { -Host::Host(const std::size_t id, const Json::Value& config, +Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, const std::function&)>& on_remove) : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" + @@ -13,6 +13,7 @@ Host::Host(const std::size_t id, const Json::Value& config, bus_name_id_(Gio::DBus::own_name(Gio::DBus::BusType::BUS_TYPE_SESSION, bus_name_, sigc::mem_fun(*this, &Host::busAcquired))), config_(config), + bar_(bar), on_add_(on_add), on_remove_(on_remove) {} @@ -136,7 +137,7 @@ void Host::addRegisteredItem(std::string service) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item(bus_name, object_path, config_)); + items_.emplace_back(new Item(bus_name, object_path, config_, bar_)); on_add_(items_.back()); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index abf95560..b504c8d0 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -1,7 +1,12 @@ #include "modules/sni/item.hpp" + +#include #include +#include #include + #include +#include template <> struct fmt::formatter : formatter { @@ -34,19 +39,31 @@ namespace waybar::modules::SNI { static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name; static const unsigned UPDATE_DEBOUNCE_TIME = 10; -Item::Item(const std::string& bn, const std::string& op, const Json::Value& config) +Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar) : bus_name(bn), object_path(op), icon_size(16), effective_icon_size(0), - icon_theme(Gtk::IconTheme::create()), - update_pending_(false) { + icon_theme(Gtk::IconTheme::create()) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } + if (config["smooth-scrolling-threshold"].isNumeric()) { + scroll_threshold_ = config["smooth-scrolling-threshold"].asDouble(); + } + if (config["show-passive-items"].isBool()) { + show_passive_ = config["show-passive-items"].asBool(); + } + + auto &window = const_cast(bar).window; + window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Item::onConfigure)); event_box.add(image); - event_box.add_events(Gdk::BUTTON_PRESS_MASK); + 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)); + // initial visibility + event_box.show_all(); + event_box.set_visible(show_passive_); cancellable_ = Gio::Cancellable::create(); @@ -60,6 +77,10 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf interface); } +void Item::onConfigure(GdkEventConfigure* ev) { + this->updateImage(); +} + void Item::proxyReady(Glib::RefPtr& result) { try { this->proxy_ = Gio::DBus::Proxy::create_for_bus_finish(result); @@ -73,12 +94,11 @@ void Item::proxyReady(Glib::RefPtr& result) { this->proxy_->signal_signal().connect(sigc::mem_fun(*this, &Item::onSignal)); - if (this->id.empty() || this->category.empty() || this->status.empty()) { + if (this->id.empty() || this->category.empty()) { spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path); return; } this->updateImage(); - // this->event_box.set_tooltip_text(this->title); } catch (const Glib::Error& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); @@ -88,10 +108,24 @@ void Item::proxyReady(Glib::RefPtr& result) { } template -T get_variant(Glib::VariantBase& value) { +T get_variant(const Glib::VariantBase& value) { return Glib::VariantBase::cast_dynamic>(value).get(); } +template <> +ToolTip get_variant(const Glib::VariantBase& value) { + ToolTip result; + // Unwrap (sa(iiay)ss) + auto container = value.cast_dynamic(value); + result.icon_name = get_variant(container.get_child(0)); + 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); + } + return result; +} + void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { try { spdlog::trace("Set tray item property: {}.{} = {}", id.empty() ? bus_name : id, name, value); @@ -102,10 +136,11 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { id = get_variant(value); } else if (name == "Title") { title = get_variant(value); + if (tooltip.text.empty()) { + event_box.set_tooltip_markup(title); + } } else if (name == "Status") { - status = get_variant(value); - } else if (name == "WindowId") { - window_id = get_variant(value); + setStatus(get_variant(value)); } else if (name == "IconName") { icon_name = get_variant(value); } else if (name == "IconPixmap") { @@ -121,7 +156,10 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "AttentionMovieName") { attention_movie_name = get_variant(value); } else if (name == "ToolTip") { - // TODO: tooltip + tooltip = get_variant(value); + if (!tooltip.text.empty()) { + event_box.set_tooltip_markup(tooltip.text); + } } else if (name == "IconThemePath") { icon_theme_path = get_variant(value); if (!icon_theme_path.empty()) { @@ -148,9 +186,22 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } } -void Item::getUpdatedProperties() { - update_pending_ = false; +void Item::setStatus(const Glib::ustring& value) { + Glib::ustring lower = value.lowercase(); + event_box.set_visible(show_passive_ || lower.compare("passive") != 0); + auto style = event_box.get_style_context(); + for (const auto& class_name : style->list_classes()) { + style->remove_class(class_name); + } + if (lower.compare("needsattention") == 0) { + // convert status to dash-case for CSS + lower = "needs-attention"; + } + style->add_class(lower); +} + +void Item::getUpdatedProperties() { auto params = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(SNI_INTERFACE_NAME)}); proxy_->call("org.freedesktop.DBus.Properties.GetAll", @@ -167,33 +218,48 @@ void Item::processUpdatedProperties(Glib::RefPtr& _result) { auto properties = properties_variant.get(); for (const auto& [name, value] : properties) { - Glib::VariantBase old_value; - proxy_->get_cached_property(old_value, name); - if (!old_value || !value.equal(old_value)) { - proxy_->set_cached_property(name, value); + if (update_pending_.count(name.raw())) { setProperty(name, const_cast(value)); } } this->updateImage(); - // this->event_box.set_tooltip_text(this->title); } catch (const Glib::Error& err) { spdlog::warn("Failed to update properties: {}", err.what()); } catch (const std::exception& err) { spdlog::warn("Failed to update properties: {}", err.what()); } + update_pending_.clear(); } +/** + * Mapping from a signal name to a set of possibly changed properties. + * Commented signals are not handled by the tray module at the moment. + */ +static const std::map> signal2props = { + {"NewTitle", {"Title"}}, + {"NewIcon", {"IconName", "IconPixmap"}}, + // {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, + // {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, + {"NewIconThemePath", {"IconThemePath"}}, + {"NewToolTip", {"ToolTip"}}, + {"NewStatus", {"Status"}}, + // {"XAyatanaNewLabel", {"XAyatanaLabel"}}, +}; + void Item::onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments) { spdlog::trace("Tray item '{}' got signal {}", id, signal_name); - if (!update_pending_ && signal_name.compare(0, 3, "New") == 0) { - /* Debounce signals and schedule update of all properties. - * Based on behavior of Plasma dataengine for StatusNotifierItem. - */ - update_pending_ = true; - Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &Item::getUpdatedProperties), - UPDATE_DEBOUNCE_TIME); + auto changed = signal2props.find(signal_name.raw()); + if (changed != signal2props.end()) { + if (update_pending_.empty()) { + /* Debounce signals and schedule update of all properties. + * Based on behavior of Plasma dataengine for StatusNotifierItem. + */ + Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &Item::getUpdatedProperties), + UPDATE_DEBOUNCE_TIME); + } + update_pending_.insert(changed->second.begin(), changed->second.end()); } } @@ -222,7 +288,11 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { if (array != nullptr) { g_free(array); } +#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68 + array = static_cast(g_memdup2(data, size)); +#else array = static_cast(g_memdup(data, size)); +#endif lwidth = width; lheight = height; } @@ -252,35 +322,41 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { } void Item::updateImage() { - image.set_from_icon_name("image-missing", Gtk::ICON_SIZE_MENU); - image.set_pixel_size(icon_size); - if (!icon_name.empty()) { - try { - // Try to find icons specified by path and filename + auto pixbuf = getIconPixbuf(); + auto scaled_icon_size = getScaledIconSize(); + + if (!pixbuf) { + pixbuf = getIconByName("image-missing", getScaledIconSize()); + } + + // If the loaded icon is not square, assume that the icon height should match the + // requested icon size, but the width is allowed to be different. As such, if the + // height of the image does not match the requested icon size, resize the icon such that + // the aspect ratio is maintained, but the height matches the requested icon size. + if (pixbuf->get_height() != scaled_icon_size) { + int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); + pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); + } + + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, 0, image.get_window()); + image.set(surface); +} + +Glib::RefPtr Item::getIconPixbuf() { + try { + if (!icon_name.empty()) { std::ifstream temp(icon_name); if (temp.is_open()) { - auto pixbuf = Gdk::Pixbuf::create_from_file(icon_name); - if (pixbuf->gobj() != nullptr) { - // An icon specified by path and filename may be the wrong size for - // the tray - // Keep the aspect ratio and scale to make the height equal to icon_size - // If people have non square icons, assume they want it to grow in width not height - int width = icon_size * pixbuf->get_width() / pixbuf->get_height(); - - pixbuf = pixbuf->scale_simple(width, icon_size, Gdk::InterpType::INTERP_BILINEAR); - image.set(pixbuf); - } - } else { - image.set(getIconByName(icon_name, icon_size)); + return Gdk::Pixbuf::create_from_file(icon_name); } - } catch (Glib::Error& e) { - spdlog::error("Item '{}': {}", id, static_cast(e.what())); + return getIconByName(icon_name, getScaledIconSize()); + } else if (icon_pixmap) { + return icon_pixmap; } - } else if (icon_pixmap) { - // An icon extracted may be the wrong size for the tray - icon_pixmap = icon_pixmap->scale_simple(icon_size, icon_size, Gdk::InterpType::INTERP_BILINEAR); - image.set(icon_pixmap); - } + } catch (Glib::Error& e) { + spdlog::error("Item '{}': {}", id, static_cast(e.what())); + } + return getIconByName("image-missing", getScaledIconSize()); } Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { @@ -315,6 +391,11 @@ Glib::RefPtr Item::getIconByName(const std::string& name, int reque name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } +double Item::getScaledIconSize() { + // apply the scale factor from the Gtk window to the requested icon size + return icon_size * image.get_scale_factor(); +} + void Item::onMenuDestroyed(Item* self, GObject* old_menu_pointer) { if (old_menu_pointer == reinterpret_cast(self->dbus_menu)) { self->gtk_menu = nullptr; @@ -360,4 +441,52 @@ bool Item::handleClick(GdkEventButton* const& ev) { return false; } +bool Item::handleScroll(GdkEventScroll* const& ev) { + int dx = 0, dy = 0; + switch (ev->direction) { + case GDK_SCROLL_UP: + dy = -1; + break; + case GDK_SCROLL_DOWN: + dy = 1; + break; + case GDK_SCROLL_LEFT: + dx = -1; + break; + case GDK_SCROLL_RIGHT: + dx = 1; + break; + case GDK_SCROLL_SMOOTH: + distance_scrolled_x_ += ev->delta_x; + distance_scrolled_y_ += ev->delta_y; + // check against the configured threshold and ensure that the absolute value >= 1 + if (distance_scrolled_x_ > scroll_threshold_) { + dx = (int)lround(std::max(distance_scrolled_x_, 1.0)); + distance_scrolled_x_ = 0; + } else if (distance_scrolled_x_ < -scroll_threshold_) { + dx = (int)lround(std::min(distance_scrolled_x_, -1.0)); + distance_scrolled_x_ = 0; + } + if (distance_scrolled_y_ > scroll_threshold_) { + dy = (int)lround(std::max(distance_scrolled_y_, 1.0)); + distance_scrolled_y_ = 0; + } else if (distance_scrolled_y_ < -scroll_threshold_) { + dy = (int)lround(std::min(distance_scrolled_y_, -1.0)); + distance_scrolled_y_ = 0; + } + break; + } + if (dx != 0) { + auto parameters = Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(dx), Glib::Variant::create("horizontal")}); + proxy_->call("Scroll", parameters); + } + if (dy != 0) { + auto parameters = Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(dy), Glib::Variant::create("vertical")}); + proxy_->call("Scroll", parameters); + } + return true; +} + } // namespace waybar::modules::SNI diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index ae3702c2..c32c0d6a 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -7,7 +7,7 @@ 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), watcher_(SNI::Watcher::getInstance()), - host_(nb_hosts_, config, std::bind(&Tray::onAdd, this, std::placeholders::_1), + host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1)) { spdlog::warn( "For a functional tray you must have libappindicator-* installed and export " @@ -35,11 +35,8 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - if (box_.get_children().empty()) { - box_.hide(); - } else { - box_.show_all(); - } + // Show tray only when items are available + box_.set_visible(!box_.get_children().empty()); // Call parent update AModule::update(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp new file mode 100644 index 00000000..186fa4bb --- /dev/null +++ b/src/modules/sway/language.cpp @@ -0,0 +1,215 @@ +#include "modules/sway/language.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "modules/sway/ipc/ipc.hpp" +#include "util/string.hpp" + +namespace waybar::modules::sway { + +const std::string Language::XKB_LAYOUT_NAMES_KEY = "xkb_layout_names"; +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) { + 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); + } + if (format_.find("{shortDescription}") != std::string::npos) { + displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); + } + if (config.isMember("tooltip-format")) { + tooltip_format_ = config["tooltip-format"].asString(); + } + ipc_.subscribe(R"(["input"])"); + ipc_.signal_event.connect(sigc::mem_fun(*this, &Language::onEvent)); + ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Language::onCmd)); + ipc_.sendCmd(IPC_GET_INPUTS); + // Launch worker + ipc_.setWorker([this] { + try { + ipc_.handleEvent(); + } catch (const std::exception& e) { + spdlog::error("Language: {}", e.what()); + } + }); + dp.emit(); +} + +void Language::onCmd(const struct Ipc::ipc_response& res) { + if (res.type != static_cast(IPC_GET_INPUTS)) { + return; + } + + try { + std::lock_guard lock(mutex_); + auto payload = parser_.parse(res.payload); + std::vector used_layouts; + // Display current layout of a device with a maximum count of layouts, expecting that all will + // be OK + Json::ArrayIndex max_id = 0, max = 0; + for (Json::ArrayIndex i = 0; i < payload.size(); i++) { + auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); + if (size > max) { + max = size; + max_id = i; + } + } + + for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) { + used_layouts.push_back(layout.asString()); + } + + init_layouts_map(used_layouts); + set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + dp.emit(); + } catch (const std::exception& e) { + spdlog::error("Language: {}", e.what()); + } +} + +void Language::onEvent(const struct Ipc::ipc_response& res) { + if (res.type != static_cast(IPC_EVENT_INPUT)) { + return; + } + + try { + std::lock_guard lock(mutex_); + auto payload = parser_.parse(res.payload)["input"]; + if (payload["type"].asString() == "keyboard") { + set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + } + dp.emit(); + } catch (const std::exception& e) { + spdlog::error("Language: {}", e.what()); + } +} + +auto Language::update() -> void { + std::lock_guard lock(mutex_); + auto display_layout = trim(fmt::format(format_, + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("long", layout_.full_name), + fmt::arg("variant", layout_.variant))); + label_.set_markup(display_layout); + if (tooltipEnabled()) { + if (tooltip_format_ != "") { + auto tooltip_display_layout = trim(fmt::format(tooltip_format_, + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("long", layout_.full_name), + fmt::arg("variant", layout_.variant))); + label_.set_tooltip_markup(tooltip_display_layout); + } else { + label_.set_tooltip_markup(display_layout); + } + } + + event_box_.show(); + + // Call parent update + ALabel::update(); +} + +auto Language::set_current_layout(std::string current_layout) -> void { + layout_ = layouts_map_[current_layout]; +} + +auto Language::init_layouts_map(const std::vector& used_layouts) -> void { + std::map> found_by_short_names; + XKBContext xkb_context; + auto layout = xkb_context.next_layout(); + for (; layout != nullptr; layout = xkb_context.next_layout()) { + if (std::find(used_layouts.begin(), used_layouts.end(), layout->full_name) == + used_layouts.end()) { + continue; + } + + if (!is_variant_displayed) { + auto short_name = layout->short_name; + if (found_by_short_names.count(short_name) > 0) { + found_by_short_names[short_name].push_back(layout); + } else { + found_by_short_names[short_name] = {layout}; + } + } + + layouts_map_.emplace(layout->full_name, *layout); + } + + if (is_variant_displayed || found_by_short_names.size() == 0) { + return; + } + + std::map short_name_to_number_map; + for (const auto& used_layout_name : used_layouts) { + auto used_layout = &layouts_map_.find(used_layout_name)->second; + auto layouts_with_same_name_list = found_by_short_names[used_layout->short_name]; + if (layouts_with_same_name_list.size() < 2) { + continue; + } + + if (short_name_to_number_map.count(used_layout->short_name) == 0) { + short_name_to_number_map[used_layout->short_name] = 1; + } + + if (displayed_short_flag != static_cast(0)) { + int& number = short_name_to_number_map[used_layout->short_name]; + used_layout->short_name = + used_layout->short_name + std::to_string(number); + used_layout->short_description = + used_layout->short_description + std::to_string(number); + ++number; + } + } +} + +Language::XKBContext::XKBContext() { + context_ = rxkb_context_new(RXKB_CONTEXT_NO_DEFAULT_INCLUDES); + rxkb_context_include_path_append_default(context_); + rxkb_context_parse_default_ruleset(context_); +} + +auto Language::XKBContext::next_layout() -> Layout* { + if (xkb_layout_ == nullptr) { + xkb_layout_ = rxkb_layout_first(context_); + } else { + xkb_layout_ = rxkb_layout_next(xkb_layout_); + } + + if (xkb_layout_ == nullptr) { + return nullptr; + } + + auto description = std::string(rxkb_layout_get_description(xkb_layout_)); + auto name = std::string(rxkb_layout_get_name(xkb_layout_)); + auto variant_ = rxkb_layout_get_variant(xkb_layout_); + std::string variant = variant_ == nullptr ? "" : std::string(variant_); + auto short_description_ = rxkb_layout_get_brief(xkb_layout_); + std::string short_description; + if (short_description_ != nullptr) { + short_description = std::string(short_description_); + base_layouts_by_name_.emplace(name, xkb_layout_); + } else { + auto base_layout = base_layouts_by_name_[name]; + short_description = base_layout == nullptr ? "" : std::string(rxkb_layout_get_brief(base_layout)); + } + delete layout_; + layout_ = new Layout{description, name, variant, short_description}; + return layout_; +} + +Language::XKBContext::~XKBContext() { + rxkb_context_unref(context_); + delete layout_; +} +} // namespace waybar::modules::sway diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index f10bf1ce..fc81b2cf 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -1,5 +1,6 @@ #include "modules/sway/window.hpp" #include +#include namespace waybar::modules::sway { @@ -56,7 +57,8 @@ auto Window::update() -> void { bar_.window.get_style_context()->remove_class("solo"); bar_.window.get_style_context()->remove_class("empty"); } - label_.set_markup(fmt::format(format_, window_)); + label_.set_markup(fmt::format(format_, fmt::arg("title", rewriteTitle(window_)), + fmt::arg("app_id", app_id_))); if (tooltipEnabled()) { label_.set_tooltip_text(window_); } @@ -64,29 +66,58 @@ auto Window::update() -> void { ALabel::update(); } -std::tuple Window::getFocusedNode( - const Json::Value& nodes, std::string& output) { - for (auto const& node : nodes) { +int leafNodesInWorkspace(const Json::Value& node) { + auto const& nodes = node["nodes"]; + auto const& floating_nodes = node["floating_nodes"]; + if(nodes.empty() && floating_nodes.empty()) { + if(node["type"] == "workspace") + return 0; + else + return 1; + } + int sum = 0; + if (!nodes.empty()) { + for(auto const& node : nodes) + sum += leafNodesInWorkspace(node); + } + if (!floating_nodes.empty()) { + for(auto const& node : floating_nodes) + sum += leafNodesInWorkspace(node); + } + return sum; +} + +std::tuple gfnWithWorkspace( + const Json::Value& nodes, std::string& output, const Json::Value& config_, + const Bar& bar_, Json::Value& parentWorkspace) { + for(auto const& node : nodes) { if (node["output"].isString()) { output = node["output"].asString(); } + // found node if (node["focused"].asBool() && (node["type"] == "con" || node["type"] == "floating_con")) { if ((!config_["all-outputs"].asBool() && output == bar_.output->name) || config_["all-outputs"].asBool()) { auto app_id = node["app_id"].isString() ? node["app_id"].asString() - : node["window_properties"]["instance"].asString(); - return {nodes.size(), - node["id"].asInt(), - Glib::Markup::escape_text(node["name"].asString()), - app_id}; + : node["window_properties"]["instance"].asString(); + int nb = node.size(); + if(parentWorkspace != 0) + nb = leafNodesInWorkspace(parentWorkspace); + return {nb, + node["id"].asInt(), + Glib::Markup::escape_text(node["name"].asString()), + app_id}; } } - auto [nb, id, name, app_id] = getFocusedNode(node["nodes"], output); + // iterate + if(node["type"] == "workspace") + parentWorkspace = node; + auto [nb, id, name, app_id] = gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace); if (id > -1 && !name.empty()) { return {nb, id, name, app_id}; } // Search for floating node - std::tie(nb, id, name, app_id) = getFocusedNode(node["floating_nodes"], output); + std::tie(nb, id, name, app_id) = gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace); if (id > -1 && !name.empty()) { return {nb, id, name, app_id}; } @@ -94,6 +125,12 @@ std::tuple Window::getFocusedNode( return {0, -1, "", ""}; } +std::tuple Window::getFocusedNode( + const Json::Value& nodes, std::string& output) { + Json::Value placeholder = 0; + return gfnWithWorkspace(nodes, output, config_, bar_, placeholder); +} + void Window::getTree() { try { ipc_.sendCmd(IPC_GET_TREE); @@ -102,4 +139,30 @@ void Window::getTree() { } } +std::string Window::rewriteTitle(const std::string& title) { + const auto& rules = config_["rewrite"]; + if (!rules.isObject()) { + return title; + } + + std::string res = title; + + for (auto it = rules.begin(); it != rules.end(); ++it) { + if (it.key().isString() && it->isString()) { + try { + // malformated regexes will cause an exception. + // in this case, log error and try the next rule. + const std::regex rule{it.key().asString()}; + if (std::regex_match(title, rule)) { + res = std::regex_replace(res, rule, it->asString()); + } + } catch (const std::regex_error& e) { + spdlog::error("Invalid rule {}: {}", it.key().asString(), e.what()); + } + } + } + + return res; +} + } // namespace waybar::modules::sway diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 60d9887a..43dcf33c 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -2,8 +2,27 @@ #include +#include +#include + namespace waybar::modules::sway { +// Helper function to 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])) { + errno = 0; + char * endptr = NULL; + 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 -1; +} + Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, !config["disable-scroll"].asBool()), bar_(bar), @@ -102,13 +121,29 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { // the "num" property (integer type): // The workspace number or -1 for workspaces that do // not start with a number. - auto l = lhs["num"].asInt(); - auto r = rhs["num"].asInt(); + // We could rely on sway providing this property: + // + // auto l = lhs["num"].asInt(); + // auto r = rhs["num"].asInt(); + // + // We cannot rely on the "num" property as provided by sway + // via IPC, because persistent workspace might not exist in + // sway's view. However, we need this property also for + // not-yet created persistent workspace. As such, we simply + // duplicate sway's logic of assigning the "num" property + // into waybar (see convertWorkspaceNameToNum). This way the + // sorting should work out even when we include workspaces + // that do not currently exist. + auto lname = lhs["name"].asString(); + auto rname = rhs["name"].asString(); + int l = convertWorkspaceNameToNum(lname); + int r = convertWorkspaceNameToNum(rname); + if (l == r) { // in case both integers are the same, lexicographical // sort. This also covers the case when both don't have a // number (i.e., l == r == -1). - return lhs["name"].asString() < rhs["name"].asString(); + return lname < rname; } // one of the workspaces doesn't begin with a number, so @@ -213,23 +248,34 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { auto pair = buttons_.emplace(node["name"].asString(), node["name"].asString()); auto &&button = pair.first->second; box_.pack_start(button, false, false, 0); + button.set_name("sway-workspace-" + node["name"].asString()); button.set_relief(Gtk::RELIEF_NONE); - button.signal_clicked().connect([this, node] { - try { - if (node["target_output"].isString()) { - ipc_.sendCmd( - IPC_COMMAND, - fmt::format("workspace \"{}\"; move workspace to output \"{}\"; workspace \"{}\"", - node["name"].asString(), - node["target_output"].asString(), - node["name"].asString())); - } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace \"{}\"", node["name"].asString())); + if (!config_["disable-click"].asBool()) { + button.signal_pressed().connect([this, node] { + try { + if (node["target_output"].isString()) { + ipc_.sendCmd( + IPC_COMMAND, + fmt::format(workspace_switch_cmd_ + "; move workspace to output \"{}\"; " + workspace_switch_cmd_, + "--no-auto-back-and-forth", + node["name"].asString(), + node["target_output"].asString(), + "--no-auto-back-and-forth", + node["name"].asString())); + } else { + ipc_.sendCmd( + IPC_COMMAND, + fmt::format("workspace {} \"{}\"", + config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : "", + node["name"].asString())); + } + } catch (const std::exception &e) { + spdlog::error("Workspaces: {}", e.what()); } - } catch (const std::exception &e) { - spdlog::error("Workspaces: {}", e.what()); - } - }); + }); + } return button; } @@ -245,12 +291,20 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node return config_["format-icons"]["persistent"].asString(); } else if (config_["format-icons"][key].isString()) { return config_["format-icons"][key].asString(); + } else if (config_["format-icons"][trimWorkspaceName(key)].isString()) { + return config_["format-icons"][trimWorkspaceName(key)].asString(); } } return name; } bool Workspaces::handleScroll(GdkEventScroll *e) { + if (gdk_event_get_pointer_emulated((GdkEvent *)e)) { + /** + * Ignore emulated scroll events on window + */ + return false; + } auto dir = AModule::getScrollDir(e); if (dir == SCROLL_DIR::NONE) { return true; @@ -276,7 +330,9 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { } } try { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace \"{}\"", name)); + 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()); } diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bf0bd1c1..84560e8d 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -6,7 +6,7 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val if (config_["hwmon-path"].isString()) { file_path_ = config_["hwmon-path"].asString(); } else if (config_["hwmon-path-abs"].isString() && config_["input-filename"].isString()) { - file_path_ = (*std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())).path().u8string() + "/" + config_["input-filename"].asString(); + file_path_ = (*std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())).path().string() + "/" + config_["input-filename"].asString(); } else { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); @@ -40,6 +40,16 @@ auto waybar::modules::Temperature::update() -> void { fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k), fmt::arg("icon", getIcon(temperature_c, "", max_temp)))); + if (tooltipEnabled()) { + std::string tooltip_format = "{temperatureC}°C"; + if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + label_.set_tooltip_text(fmt::format(tooltip_format, + fmt::arg("temperatureC", temperature_c), + fmt::arg("temperatureF", temperature_f), + fmt::arg("temperatureK", temperature_k))); + } // Call parent update ALabel::update(); } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp new file mode 100644 index 00000000..af1b0da0 --- /dev/null +++ b/src/modules/wlr/taskbar.cpp @@ -0,0 +1,866 @@ +#include "modules/wlr/taskbar.hpp" + +#include "glibmm/error.h" +#include "glibmm/fileutils.h" +#include "glibmm/refptr.h" +#include "util/format.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include + + +namespace waybar::modules::wlr { + +/* String manipulation methods */ +const std::string WHITESPACE = " \n\r\t\f\v"; + +static std::string ltrim(const std::string& s) +{ + size_t start = s.find_first_not_of(WHITESPACE); + return (start == std::string::npos) ? "" : s.substr(start); +} + +static std::string rtrim(const std::string& s) +{ + size_t end = s.find_last_not_of(WHITESPACE); + return (end == std::string::npos) ? "" : s.substr(0, end + 1); +} + +static std::string trim(const std::string& s) +{ + return rtrim(ltrim(s)); +} + + +/* Icon loading functions */ +static std::vector search_prefix() +{ + std::vector prefixes = {""}; + + auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); + if (!xdg_data_dirs) { + prefixes.emplace_back("/usr/share/"); + prefixes.emplace_back("/usr/local/share/"); + } else { + std::string xdg_data_dirs_str(xdg_data_dirs); + size_t start = 0, end = 0; + + do { + end = xdg_data_dirs_str.find(':', start); + auto p = xdg_data_dirs_str.substr(start, end-start); + prefixes.push_back(trim(p) + "/"); + + start = end == std::string::npos ? end : end + 1; + } while(end != std::string::npos); + } + + 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; +} + +static Glib::RefPtr load_icon_from_file(std::string icon_path, int size) +{ + try { + auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size); + return pb; + } catch(...) { + return {}; + } +} + +/* Method 1 - get the correct icon name from the desktop file */ +static std::string get_from_desktop_app_info(const std::string &app_id) +{ + static std::vector prefixes = search_prefix(); + + std::vector app_folders = { + "", + "applications/", + "applications/kde/", + "applications/org.kde." + }; + + std::vector suffixes = { + "", + ".desktop" + }; + + Glib::RefPtr app_info; + + for (auto& prefix : prefixes) + for (auto& folder : app_folders) + for (auto& suffix : suffixes) + if (!app_info) + app_info = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); + + if (app_info && app_info->get_icon()) + return app_info->get_icon()->to_string(); + + return ""; +} + +/* Method 2 - use the app_id and check whether there is an icon with this name in the icon theme */ +static std::string get_from_icon_theme(const Glib::RefPtr& icon_theme, + const std::string &app_id) +{ + if (icon_theme->lookup_icon(app_id, 24)) + return app_id; + + return ""; +} + +/* Method 3 - as last resort perform a search for most appropriate desktop info file */ +static std::string get_from_desktop_app_info_search(const std::string &app_id) +{ + std::string desktop_file = ""; + + gchar*** desktop_list = g_desktop_app_info_search(app_id.c_str()); + if (desktop_list != nullptr && desktop_list[0] != nullptr) { + for (size_t i=0; desktop_list[0][i]; i++) { + if (desktop_file == "") { + desktop_file = desktop_list[0][i]; + } else { + auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); + auto startup_class = tmp_info->get_startup_wm_class(); + + if (startup_class == app_id) { + desktop_file = desktop_list[0][i]; + break; + } + } + } + g_strfreev(desktop_list[0]); + } + g_free(desktop_list); + + return get_from_desktop_app_info(desktop_file); +} + +static bool image_load_icon(Gtk::Image& image, const Glib::RefPtr& icon_theme, + const std::string &app_id_list, int size) +{ + std::string app_id; + std::istringstream stream(app_id_list); + bool found = false; + + /* Wayfire sends a list of app-id's in space separated format, other compositors + * send a single app-id, but in any case this works fine */ + while (stream >> app_id) + { + size_t start = 0, end = app_id.size(); + start = app_id.rfind(".", end); + std::string app_name = app_id.substr(start+1, app_id.size()); + + auto lower_app_id = app_id; + std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), + [](char c){ return std::tolower(c); }); + + std::string icon_name = get_from_icon_theme(icon_theme, app_id); + + if (icon_name.empty()) + icon_name = get_from_icon_theme(icon_theme, lower_app_id); + if (icon_name.empty()) + icon_name = get_from_icon_theme(icon_theme, app_name); + if (icon_name.empty()) + icon_name = get_from_desktop_app_info(app_id); + if (icon_name.empty()) + icon_name = get_from_desktop_app_info(lower_app_id); + if (icon_name.empty()) + icon_name = get_from_desktop_app_info(app_name); + if (icon_name.empty()) + icon_name = get_from_desktop_app_info_search(app_id); + + if (icon_name.empty()) + icon_name = "unknown"; + + Glib::RefPtr pixbuf; + + try { + pixbuf = icon_theme->load_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch(...) { + if (Glib::file_test(icon_name, Glib::FILE_TEST_EXISTS)) + pixbuf = load_icon_from_file(icon_name, size); + else + pixbuf = {}; + } + + if (pixbuf) { + image.set(pixbuf); + found = true; + break; + } + } + + return found; +} + +/* Task class implementation */ +uint32_t Task::global_id = 0; + +static void tl_handle_title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + const char *title) +{ + return static_cast(data)->handle_title(title); +} + +static void tl_handle_app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + const char *app_id) +{ + return static_cast(data)->handle_app_id(app_id); +} + +static void tl_handle_output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_output *output) +{ + return static_cast(data)->handle_output_enter(output); +} + +static void tl_handle_output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_output *output) +{ + return static_cast(data)->handle_output_leave(output); +} + +static void tl_handle_state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct wl_array *state) +{ + return static_cast(data)->handle_state(state); +} + +static void tl_handle_done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + return static_cast(data)->handle_done(); +} + +static void tl_handle_parent(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, + struct zwlr_foreign_toplevel_handle_v1 *parent) +{ + /* This is explicitly left blank */ +} + +static void tl_handle_closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + return static_cast(data)->handle_closed(); +} + +static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_handle_impl = { + .title = tl_handle_title, + .app_id = tl_handle_app_id, + .output_enter = tl_handle_output_enter, + .output_leave = tl_handle_output_leave, + .state = tl_handle_state, + .done = tl_handle_done, + .closed = tl_handle_closed, + .parent = tl_handle_parent, +}; + +Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, + struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) : + bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat}, + id_{global_id++}, + content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} +{ + zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); + + button_.set_relief(Gtk::RELIEF_NONE); + + content_.add(text_before_); + content_.add(icon_); + content_.add(text_after_); + + content_.show(); + button_.add(content_); + + with_icon_ = false; + format_before_.clear(); + format_after_.clear(); + + if (config_["format"].isString()) { + /* The user defined a format string, use it */ + auto format = config_["format"].asString(); + + auto icon_pos = format.find("{icon}"); + if (icon_pos == 0) { + with_icon_ = true; + format_after_ = format.substr(6); + } else if (icon_pos == std::string::npos) { + format_before_ = format; + } else { + with_icon_ = true; + format_before_ = format.substr(0, icon_pos); + format_after_ = format.substr(icon_pos + 6); + } + } else { + /* The default is to only show the icon */ + with_icon_ = true; + } + + /* Strip spaces at the beginning and end of the format strings */ + format_tooltip_.clear(); + if (!config_["tooltip"].isBool() || config_["tooltip"].asBool()) { + if (config_["tooltip-format"].isString()) + format_tooltip_ = config_["tooltip-format"].asString(); + else + format_tooltip_ = "{title}"; + } + + /* Handle click events if configured */ + if (config_["on-click"].isString() || config_["on-click-middle"].isString() + || config_["on-click-right"].isString()) { + button_.add_events(Gdk::BUTTON_PRESS_MASK); + button_.signal_button_press_event().connect( + sigc::mem_fun(*this, &Task::handle_clicked), false); + } +} + +Task::~Task() +{ + if (handle_) { + zwlr_foreign_toplevel_handle_v1_destroy(handle_); + handle_ = nullptr; + } + if (button_visible_) { + tbar_->remove_button(button_); + button_visible_ = false; + } +} + +std::string Task::repr() const +{ + std::stringstream ss; + ss << "Task (" << id_ << ") " << title_ << " [" << app_id_ << "] <" + << (active() ? "A" : "a") + << (maximized() ? "M" : "m") + << (minimized() ? "I" : "i") + << (fullscreen() ? "F" : "f") + << ">"; + + return ss.str(); +} + +std::string Task::state_string(bool shortened) const +{ + std::stringstream ss; + if (shortened) + ss << (minimized() ? "m" : "") << (maximized() ? "M" : "") + << (active() ? "A" : "") << (fullscreen() ? "F" : ""); + else + ss << (minimized() ? "minimized " : "") << (maximized() ? "maximized " : "") + << (active() ? "active " : "") << (fullscreen() ? "fullscreen " : ""); + + std::string res = ss.str(); + if (shortened || res.empty()) + return res; + else + return res.substr(0, res.size() - 1); +} + +void Task::handle_title(const char *title) +{ + title_ = title; + hide_if_ignored(); +} + +void Task::hide_if_ignored() +{ + if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) { + ignored_ = true; + if (button_visible_) { + auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + handle_output_leave(output); + } + } else { + bool is_was_ignored = ignored_; + ignored_ = false; + if (is_was_ignored) { + auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + handle_output_enter(output); + } + } +} + +void Task::handle_app_id(const char *app_id) +{ + app_id_ = app_id; + hide_if_ignored(); + + 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_id_, icon_size)) { + found = true; + break; + } + } + + if (found) + icon_.show(); + else + spdlog::debug("Couldn't find icon for {}", app_id_); +} + +void Task::handle_output_enter(struct wl_output *output) +{ + if (ignored_) { + spdlog::debug("{} is ignored", repr()); + return; + } + + spdlog::debug("{} entered output {}", repr(), (void*)output); + + if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { + /* The task entered the output of the current bar make the button visible */ + tbar_->add_button(button_); + button_.show(); + button_visible_ = true; + spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + } +} + +void Task::handle_output_leave(struct wl_output *output) +{ + spdlog::debug("{} left output {}", repr(), (void*)output); + + if (button_visible_ && !tbar_->all_outputs() && tbar_->show_output(output)) { + /* The task left the output of the current bar, make the button invisible */ + tbar_->remove_button(button_); + button_.hide(); + button_visible_ = false; + spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + } +} + +void Task::handle_state(struct wl_array *state) +{ + state_ = 0; + size_t size = state->size / sizeof(uint32_t); + for (size_t i = 0; i < size; ++i) { + auto entry = static_cast(state->data)[i]; + if (entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED) + state_ |= MAXIMIZED; + if (entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED) + state_ |= MINIMIZED; + if (entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) + state_ |= ACTIVE; + if (entry == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) + state_ |= FULLSCREEN; + } +} + +void Task::handle_done() +{ + spdlog::debug("{} changed", repr()); + + if (state_ & MAXIMIZED) { + button_.get_style_context()->add_class("maximized"); + } else if (!(state_ & MAXIMIZED)) { + button_.get_style_context()->remove_class("maximized"); + } + + if (state_ & MINIMIZED) { + button_.get_style_context()->add_class("minimized"); + } else if (!(state_ & MINIMIZED)) { + button_.get_style_context()->remove_class("minimized"); + } + + if (state_ & ACTIVE) { + button_.get_style_context()->add_class("active"); + } else if (!(state_ & ACTIVE)) { + button_.get_style_context()->remove_class("active"); + } + + if (state_ & FULLSCREEN) { + button_.get_style_context()->add_class("fullscreen"); + } else if (!(state_ & FULLSCREEN)) { + button_.get_style_context()->remove_class("fullscreen"); + } + + if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) + tbar_->move_button(button_, 0); + + tbar_->dp.emit(); +} + +void Task::handle_closed() +{ + spdlog::debug("{} closed", repr()); + zwlr_foreign_toplevel_handle_v1_destroy(handle_); + handle_ = nullptr; + if (button_visible_) { + tbar_->remove_button(button_); + button_visible_ = false; + } + tbar_->remove_task(id_); +} + +bool Task::handle_clicked(GdkEventButton *bt) +{ + std::string action; + if (config_["on-click"].isString() && bt->button == 1) + action = config_["on-click"].asString(); + else if (config_["on-click-middle"].isString() && bt->button == 2) + action = config_["on-click-middle"].asString(); + else if (config_["on-click-right"].isString() && bt->button == 3) + action = config_["on-click-right"].asString(); + + if (action.empty()) + return true; + else if (action == "activate") + activate(); + else if (action == "minimize") + minimize(!minimized()); + else if (action == "minimize-raise"){ + if (minimized()) + minimize(false); + else if (active()) + minimize(true); + else + activate(); + } + else if (action == "maximize") + maximize(!maximized()); + else if (action == "fullscreen") + fullscreen(!fullscreen()); + else if (action == "close") + close(); + else + spdlog::warn("Unknown action {}", action); + + return true; +} + +bool Task::operator==(const Task &o) const +{ + return o.id_ == id_; +} + +bool Task::operator!=(const Task &o) const +{ + return o.id_ != id_; +} + +void Task::update() +{ + bool markup = config_["markup"].isBool() ? config_["markup"].asBool() : false; + std::string title = title_; + std::string app_id = app_id_; + if (markup) { + title = Glib::Markup::escape_text(title); + app_id = Glib::Markup::escape_text(app_id); + } + if (!format_before_.empty()) { + auto txt = fmt::format(format_before_, + fmt::arg("title", title), + fmt::arg("app_id", app_id), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ); + if (markup) + text_before_.set_markup(txt); + else + text_before_.set_label(txt); + text_before_.show(); + } + if (!format_after_.empty()) { + auto txt = fmt::format(format_after_, + fmt::arg("title", title), + fmt::arg("app_id", app_id), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ); + if (markup) + text_after_.set_markup(txt); + else + text_after_.set_label(txt); + text_after_.show(); + } + + if (!format_tooltip_.empty()) { + auto txt = fmt::format(format_tooltip_, + fmt::arg("title", title), + fmt::arg("app_id", app_id), + fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true)) + ); + if (markup) + button_.set_tooltip_markup(txt); + else + button_.set_tooltip_text(txt); + } +} + +void Task::maximize(bool set) +{ + if (set) + zwlr_foreign_toplevel_handle_v1_set_maximized(handle_); + else + zwlr_foreign_toplevel_handle_v1_unset_maximized(handle_); +} + +void Task::minimize(bool set) +{ + if (set) + zwlr_foreign_toplevel_handle_v1_set_minimized(handle_); + else + zwlr_foreign_toplevel_handle_v1_unset_minimized(handle_); +} + +void Task::activate() +{ + zwlr_foreign_toplevel_handle_v1_activate(handle_, seat_); +} + +void Task::fullscreen(bool set) +{ + if (zwlr_foreign_toplevel_handle_v1_get_version(handle_) < ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN_SINCE_VERSION) { + spdlog::warn("Foreign toplevel manager server does not support for set/unset fullscreen."); + return; + } + + if (set) + zwlr_foreign_toplevel_handle_v1_set_fullscreen(handle_, nullptr); + else + zwlr_foreign_toplevel_handle_v1_unset_fullscreen(handle_); +} + +void Task::close() +{ + zwlr_foreign_toplevel_handle_v1_close(handle_); +} + + +/* Taskbar class implementation */ +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { + static_cast(data)->register_manager(registry, name, version); + } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { + static_cast(data)->register_seat(registry, name, version); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + /* Nothing to do here */ +} + +static const wl_registry_listener registry_listener_impl = { + .global = handle_global, + .global_remove = handle_global_remove +}; + +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}, + manager_{nullptr}, seat_{nullptr} +{ + box_.set_name("taskbar"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + 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 (!manager_) { + spdlog::error("Failed to register as toplevel manager"); + return; + } + if (!seat_) { + spdlog::error("Failed to get wayland seat"); + return; + } + + /* Get the configured icon theme if specified */ + if (config_["icon-theme"].isArray()) { + for (auto& c : config_["icon-theme"]) { + auto it_name = c.asString(); + + auto it = Gtk::IconTheme::create(); + it->set_custom_theme(it_name); + spdlog::debug("Use custom icon theme: {}", it_name); + + icon_themes_.push_back(it); + } + } else if (config_["icon-theme"].isString()) { + auto it_name = config_["icon-theme"].asString(); + + auto it = Gtk::IconTheme::create(); + it->set_custom_theme(it_name); + spdlog::debug("Use custom icon theme: {}", it_name); + + icon_themes_.push_back(it); + } + + // Load ignore-list + if (config_["ignore-list"].isArray()) { + for (auto& app_name : config_["ignore-list"]) { + ignore_list_.emplace(app_name.asString()); + } + } + + icon_themes_.push_back(Gtk::IconTheme::get_default()); +} + +Taskbar::~Taskbar() +{ + if (manager_) { + struct wl_display *display = Client::inst()->wl_display; + /* + * Send `stop` request and wait for one roundtrip. + * This is not quite correct as the protocol encourages us to wait for the .finished event, + * but it should work with wlroots foreign toplevel manager implementation. + */ + zwlr_foreign_toplevel_manager_v1_stop(manager_); + wl_display_roundtrip(display); + + if (manager_) { + spdlog::warn("Foreign toplevel manager destroyed before .finished event"); + zwlr_foreign_toplevel_manager_v1_destroy(manager_); + manager_ = nullptr; + } + } +} + +void Taskbar::update() +{ + for (auto& t : tasks_) { + t->update(); + } + + AModule::update(); +} + +static void tm_handle_toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager, + struct zwlr_foreign_toplevel_handle_v1 *tl_handle) +{ + return static_cast(data)->handle_toplevel_create(tl_handle); +} + +static void tm_handle_finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager) +{ + return static_cast(data)->handle_finished(); +} + +static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_impl = { + .toplevel = tm_handle_toplevel, + .finished = tm_handle_finished, +}; + +void Taskbar::register_manager(struct wl_registry *registry, uint32_t name, uint32_t version) +{ + if (manager_) { + spdlog::warn("Register foreign toplevel manager again although already existing!"); + return; + } + if (version < ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_SET_FULLSCREEN_SINCE_VERSION) { + spdlog::warn("Foreign toplevel manager server does not have the appropriate version." + " To be able to use all features, you need at least version 2, but server is version {}", version); + } + + // limit version to a highest supported by the client protocol file + version = std::min(version, zwlr_foreign_toplevel_manager_v1_interface.version); + + manager_ = static_cast(wl_registry_bind(registry, name, + &zwlr_foreign_toplevel_manager_v1_interface, version)); + + if (manager_) + zwlr_foreign_toplevel_manager_v1_add_listener(manager_, &toplevel_manager_impl, this); + else + spdlog::debug("Failed to register manager"); +} + +void Taskbar::register_seat(struct wl_registry *registry, uint32_t name, uint32_t version) +{ + if (seat_) { + spdlog::warn("Register seat again although already existing!"); + return; + } + version = std::min(version, wl_seat_interface.version); + + seat_ = static_cast(wl_registry_bind(registry, name, &wl_seat_interface, version)); +} + +void Taskbar::handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1 *tl_handle) +{ + tasks_.push_back(std::make_unique(bar_, config_, this, tl_handle, seat_)); +} + +void Taskbar::handle_finished() +{ + zwlr_foreign_toplevel_manager_v1_destroy(manager_); + manager_ = nullptr; +} + +void Taskbar::add_button(Gtk::Button &bt) +{ + box_.pack_start(bt, false, false); +} + +void Taskbar::move_button(Gtk::Button &bt, int pos) +{ + box_.reorder_child(bt, pos); +} + +void Taskbar::remove_button(Gtk::Button &bt) +{ + box_.remove(bt); +} + +void Taskbar::remove_task(uint32_t id) +{ + auto it = std::find_if(std::begin(tasks_), std::end(tasks_), + [id](const TaskPtr &p) { return p->id() == id; }); + + if (it == std::end(tasks_)) { + spdlog::warn("Can't find task with id {}", id); + return; + } + + tasks_.erase(it); +} + +bool Taskbar::show_output(struct wl_output *output) const +{ + return output == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); +} + +bool Taskbar::all_outputs() const +{ + return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); +} + +std::vector> Taskbar::icon_themes() const +{ + return icon_themes_; +} +const std::unordered_set &Taskbar::ignore_list() const { return ignore_list_; } + +} /* namespace waybar::modules::wlr */ diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp new file mode 100644 index 00000000..bacef355 --- /dev/null +++ b/src/modules/wlr/workspace_manager.cpp @@ -0,0 +1,472 @@ +#include "modules/wlr/workspace_manager.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "gtkmm/widget.h" +#include "modules/wlr/workspace_manager_binding.hpp" + +namespace waybar::modules::wlr { + +uint32_t WorkspaceGroup::workspace_global_id = 0; +uint32_t WorkspaceManager::group_global_id = 0; +std::map Workspace::icons_map_; + +WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar, + const Json::Value &config) + : waybar::AModule(config, "workspaces", id, false, false), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + auto config_sort_by_name = config_["sort-by-name"]; + if (config_sort_by_name.isBool()) { + sort_by_name_ = config_sort_by_name.asBool(); + } + + auto config_sort_by_coordinates = config_["sort-by-coordinates"]; + if (config_sort_by_coordinates.isBool()) { + sort_by_coordinates_ = config_sort_by_coordinates.asBool(); + } + + auto config_all_outputs = config_["all-outputs"]; + if (config_all_outputs.isBool()) { + all_outputs_ = config_all_outputs.asBool(); + } + + auto config_active_only = config_["active-only"]; + if (config_active_only.isBool()) { + active_only_ = config_active_only.asBool(); + creation_delayed_ = active_only_; + } + + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + add_registry_listener(this); + if (!workspace_manager_) { + return; + } +} + +auto WorkspaceManager::workspace_comparator() const + -> std::function &, std::unique_ptr &)> { + return [=](std::unique_ptr &lhs, std::unique_ptr &rhs) { + auto is_name_less = lhs->get_name() < rhs->get_name(); + auto is_name_eq = lhs->get_name() == rhs->get_name(); + auto is_coords_less = lhs->get_coords() < rhs->get_coords(); + if (sort_by_name_) { + if (sort_by_coordinates_) { + return is_name_eq ? is_coords_less : is_name_less; + } else { + return is_name_less; + } + } + + if (sort_by_coordinates_) { + return is_coords_less; + } + + return lhs->id() < rhs->id(); + }; +} + +auto WorkspaceManager::sort_workspaces() -> void { + std::vector>> all_workspaces; + for (auto &group : groups_) { + auto &group_workspaces = group->workspaces(); + all_workspaces.reserve(all_workspaces.size() + + std::distance(group_workspaces.begin(), group_workspaces.end())); + if (!active_only()) { + all_workspaces.insert(all_workspaces.end(), group_workspaces.begin(), group_workspaces.end()); + continue; + } + + for (auto &workspace : group_workspaces) { + if (!workspace->is_active()) { + continue; + } + + all_workspaces.push_back(workspace); + } + } + + std::sort(all_workspaces.begin(), all_workspaces.end(), workspace_comparator()); + for (size_t i = 0; i < all_workspaces.size(); ++i) { + box_.reorder_child(all_workspaces[i].get()->get_button_ref(), i); + } +} + +auto WorkspaceManager::register_manager(wl_registry *registry, uint32_t name, uint32_t version) + -> void { + if (workspace_manager_) { + spdlog::warn("Register workspace manager again although already registered!"); + return; + } + if (version != 1) { + spdlog::warn("Using different workspace manager protocol version: {}", version); + } + workspace_manager_ = workspace_manager_bind(registry, name, version, this); +} + +auto WorkspaceManager::handle_workspace_group_create( + zext_workspace_group_handle_v1 *workspace_group_handle) -> void { + auto new_id = ++group_global_id; + groups_.push_back( + std::make_unique(bar_, box_, config_, *this, workspace_group_handle, new_id)); + spdlog::debug("Workspace group {} created", new_id); +} + +auto WorkspaceManager::handle_finished() -> void { + zext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; +} + +auto WorkspaceManager::handle_done() -> void { + for (auto &group : groups_) { + group->handle_done(); + } + dp.emit(); +} + +auto WorkspaceManager::update() -> void { + for (auto &group : groups_) { + group->update(); + } + if (creation_delayed()) { + creation_delayed_ = false; + sort_workspaces(); + } + AModule::update(); +} + +WorkspaceManager::~WorkspaceManager() { + if (!workspace_manager_) { + return; + } + + zext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; +} + +auto WorkspaceManager::remove_workspace_group(uint32_t id) -> void { + auto it = std::find_if(groups_.begin(), + groups_.end(), + [id](const std::unique_ptr &g) { return g->id() == id; }); + + if (it == groups_.end()) { + spdlog::warn("Can't find group with id {}", id); + return; + } + + groups_.erase(it); +} +auto WorkspaceManager::commit() -> void { zext_workspace_manager_v1_commit(workspace_manager_); } + +WorkspaceGroup::WorkspaceGroup(const Bar &bar, Gtk::Box &box, const Json::Value &config, + WorkspaceManager &manager, + zext_workspace_group_handle_v1 *workspace_group_handle, uint32_t id) + : bar_(bar), + box_(box), + config_(config), + workspace_manager_(manager), + workspace_group_handle_(workspace_group_handle), + id_(id) { + add_workspace_group_listener(workspace_group_handle, this); +} + +auto WorkspaceGroup::active_only() const -> bool { return workspace_manager_.active_only(); } +auto WorkspaceGroup::creation_delayed() const -> bool { + return workspace_manager_.creation_delayed(); +} + +auto WorkspaceGroup::add_button(Gtk::Button &button) -> void { + box_.pack_start(button, false, false); +} + +WorkspaceGroup::~WorkspaceGroup() { + if (!workspace_group_handle_) { + return; + } + + zext_workspace_group_handle_v1_destroy(workspace_group_handle_); + workspace_group_handle_ = nullptr; +} + +auto WorkspaceGroup::handle_workspace_create(zext_workspace_handle_v1 *workspace) -> void { + auto new_id = ++workspace_global_id; + workspaces_.push_back(std::make_unique(bar_, config_, *this, workspace, new_id)); + spdlog::debug("Workspace {} created", new_id); +} + +auto WorkspaceGroup::handle_remove() -> void { + zext_workspace_group_handle_v1_destroy(workspace_group_handle_); + workspace_group_handle_ = nullptr; + workspace_manager_.remove_workspace_group(id_); +} + +auto WorkspaceGroup::handle_output_enter(wl_output *output) -> void { + spdlog::debug("Output {} assigned to {} group", (void *)output, id_); + output_ = output; + + if (!is_visible() || workspace_manager_.creation_delayed()) { + return; + } + + for (auto &workspace : workspaces_) { + add_button(workspace->get_button_ref()); + } +} + +auto WorkspaceGroup::is_visible() const -> bool { + return output_ != nullptr && + (workspace_manager_.all_outputs() || + output_ == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj())); +} + +auto WorkspaceGroup::handle_output_leave() -> void { + spdlog::debug("Output {} remove from {} group", (void *)output_, id_); + output_ = nullptr; + + if (output_ != gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj())) { + return; + } + + for (auto &workspace : workspaces_) { + remove_button(workspace->get_button_ref()); + } +} + +auto WorkspaceGroup::update() -> void { + for (auto &workspace : workspaces_) { + if (workspace_manager_.creation_delayed()) { + add_button(workspace->get_button_ref()); + if (is_visible() && (workspace->is_active() || workspace->is_urgent())) { + workspace->show(); + } + } + + workspace->update(); + } +} + +auto WorkspaceGroup::remove_workspace(uint32_t id) -> void { + auto it = std::find_if(workspaces_.begin(), + workspaces_.end(), + [id](const std::unique_ptr &w) { return w->id() == id; }); + + if (it == workspaces_.end()) { + spdlog::warn("Can't find workspace with id {}", id); + return; + } + + workspaces_.erase(it); +} + +auto WorkspaceGroup::handle_done() -> void { + need_to_sort = false; + if (!is_visible()) { + return; + } + + for (auto &workspace : workspaces_) { + workspace->handle_done(); + } + + if (creation_delayed()) { + return; + } + + if (!workspace_manager_.all_outputs()) { + sort_workspaces(); + } else { + workspace_manager_.sort_workspaces(); + } +} + +auto WorkspaceGroup::commit() -> void { workspace_manager_.commit(); } + +auto WorkspaceGroup::sort_workspaces() -> void { + std::sort(workspaces_.begin(), workspaces_.end(), workspace_manager_.workspace_comparator()); + for (size_t i = 0; i < workspaces_.size(); ++i) { + box_.reorder_child(workspaces_[i]->get_button_ref(), i); + } +} + +auto WorkspaceGroup::remove_button(Gtk::Button &button) -> void { box_.remove(button); } + +Workspace::Workspace(const Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, + zext_workspace_handle_v1 *workspace, uint32_t id) + : bar_(bar), + config_(config), + workspace_group_(workspace_group), + workspace_handle_(workspace), + id_(id) { + add_workspace_listener(workspace, this); + + auto config_format = config["format"]; + + format_ = config_format.isString() ? config_format.asString() : "{name}"; + with_icon_ = format_.find("{icon}") != std::string::npos; + + if (with_icon_ && icons_map_.empty()) { + auto format_icons = config["format-icons"]; + for (auto &name : format_icons.getMemberNames()) { + icons_map_.emplace(name, format_icons[name].asString()); + } + } + + /* Handle click events if configured */ + if (config_["on-click"].isString() || config_["on-click-middle"].isString() || + config_["on-click-right"].isString()) { + button_.add_events(Gdk::BUTTON_PRESS_MASK); + button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked), + false); + } + + button_.set_relief(Gtk::RELIEF_NONE); + content_.set_center_widget(label_); + button_.add(content_); + + if (!workspace_group.is_visible()) { + return; + } + + workspace_group.add_button(button_); + button_.show_all(); +} + +Workspace::~Workspace() { + workspace_group_.remove_button(button_); + if (!workspace_handle_) { + return; + } + + zext_workspace_handle_v1_destroy(workspace_handle_); + workspace_handle_ = nullptr; +} + +auto Workspace::update() -> void { + label_.set_markup(fmt::format( + format_, fmt::arg("name", name_), fmt::arg("icon", with_icon_ ? get_icon() : ""))); +} + +auto Workspace::handle_state(const std::vector &state) -> void { + state_ = 0; + for (auto state_entry : state) { + switch (state_entry) { + case ZEXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE: + state_ |= (uint32_t)State::ACTIVE; + break; + case ZEXT_WORKSPACE_HANDLE_V1_STATE_URGENT: + state_ |= (uint32_t)State::URGENT; + break; + case ZEXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN: + state_ |= (uint32_t)State::HIDDEN; + break; + } + } +} + +auto Workspace::handle_remove() -> void { + zext_workspace_handle_v1_destroy(workspace_handle_); + workspace_handle_ = nullptr; + workspace_group_.remove_workspace(id_); +} + +auto add_or_remove_class(Glib::RefPtr context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +auto Workspace::handle_done() -> void { + spdlog::debug("Workspace {} changed to state {}", id_, state_); + auto style_context = button_.get_style_context(); + add_or_remove_class(style_context, is_active(), "active"); + add_or_remove_class(style_context, is_urgent(), "urgent"); + add_or_remove_class(style_context, is_hidden(), "hidden"); + + if (workspace_group_.creation_delayed()) { + return; + } + + if (workspace_group_.active_only() && (is_active() || is_urgent())) { + button_.show_all(); + } else if (workspace_group_.active_only() && !(is_active() || is_urgent())) { + button_.hide(); + } +} + +auto Workspace::get_icon() -> std::string { + if (is_active()) { + auto active_icon_it = icons_map_.find("active"); + if (active_icon_it != icons_map_.end()) { + return active_icon_it->second; + } + } + + auto named_icon_it = icons_map_.find(name_); + if (named_icon_it != icons_map_.end()) { + return named_icon_it->second; + } + + auto default_icon_it = icons_map_.find("default"); + if (default_icon_it != icons_map_.end()) { + return default_icon_it->second; + } + + return name_; +} + +auto Workspace::handle_clicked(GdkEventButton *bt) -> bool { + std::string action; + if (config_["on-click"].isString() && bt->button == 1) { + action = config_["on-click"].asString(); + } else if (config_["on-click-middle"].isString() && bt->button == 2) { + action = config_["on-click-middle"].asString(); + } else if (config_["on-click-right"].isString() && bt->button == 3) { + action = config_["on-click-right"].asString(); + } + + if (action.empty()) + return true; + else if (action == "activate") { + zext_workspace_handle_v1_activate(workspace_handle_); + } else if (action == "close") { + zext_workspace_handle_v1_remove(workspace_handle_); + } else { + spdlog::warn("Unknown action {}", action); + } + + workspace_group_.commit(); + + return true; +} + +auto Workspace::show() -> void { button_.show_all(); } +auto Workspace::hide() -> void { button_.hide(); } + +auto Workspace::handle_name(const std::string &name) -> void { + if (name_ != name) { + workspace_group_.set_need_to_sort(); + } + name_ = name; +} + +auto Workspace::handle_coordinates(const std::vector &coordinates) -> void { + if (coordinates_ != coordinates) { + workspace_group_.set_need_to_sort(); + } + coordinates_ = coordinates; +} +} // namespace waybar::modules::wlr diff --git a/src/modules/wlr/workspace_manager_binding.cpp b/src/modules/wlr/workspace_manager_binding.cpp new file mode 100644 index 00000000..30c60413 --- /dev/null +++ b/src/modules/wlr/workspace_manager_binding.cpp @@ -0,0 +1,135 @@ +#include "modules/wlr/workspace_manager_binding.hpp" + +#include +#include + +#include "client.hpp" +#include "modules/wlr/workspace_manager.hpp" + +namespace waybar::modules::wlr { + +static void handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface, + uint32_t version) { + if (std::strcmp(interface, zext_workspace_manager_v1_interface.name) == 0) { + static_cast(data)->register_manager(registry, name, version); + } +} + +static void handle_global_remove(void *data, wl_registry *registry, uint32_t name) { + /* Nothing to do here */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +void add_registry_listener(void *data) { + wl_display * display = Client::inst()->wl_display; + wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, ®istry_listener_impl, data); + wl_display_roundtrip(display); + wl_display_roundtrip(display); +} + +static void workspace_manager_handle_workspace_group( + void *data, zext_workspace_manager_v1 *_, zext_workspace_group_handle_v1 *workspace_group) { + static_cast(data)->handle_workspace_group_create(workspace_group); +} + +static void workspace_manager_handle_done(void *data, zext_workspace_manager_v1 *_) { + static_cast(data)->handle_done(); +} + +static void workspace_manager_handle_finished(void *data, zext_workspace_manager_v1 *_) { + static_cast(data)->handle_finished(); +} + +static const zext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_workspace_group, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + +zext_workspace_manager_v1 *workspace_manager_bind(wl_registry *registry, uint32_t name, + uint32_t version, void *data) { + auto *workspace_manager = static_cast( + wl_registry_bind(registry, name, &zext_workspace_manager_v1_interface, version)); + + if (workspace_manager) + zext_workspace_manager_v1_add_listener(workspace_manager, &workspace_manager_impl, data); + else + spdlog::error("Failed to register manager"); + + return workspace_manager; +} + +static void workspace_group_handle_output_enter(void *data, zext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_enter(output); +} + +static void workspace_group_handle_output_leave(void *data, zext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_leave(); +} + +static void workspace_group_handle_workspace(void *data, zext_workspace_group_handle_v1 *_, + zext_workspace_handle_v1 *workspace) { + static_cast(data)->handle_workspace_create(workspace); +} + +static void workspace_group_handle_remove(void *data, zext_workspace_group_handle_v1 *_) { + static_cast(data)->handle_remove(); +} + +static const zext_workspace_group_handle_v1_listener workspace_group_impl = { + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace = workspace_group_handle_workspace, + .remove = workspace_group_handle_remove}; + +void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle, + void * data) { + zext_workspace_group_handle_v1_add_listener(workspace_group_handle, &workspace_group_impl, data); +} + +void workspace_handle_name(void *data, struct zext_workspace_handle_v1 *_, const char *name) { + static_cast(data)->handle_name(name); +} + +void workspace_handle_coordinates(void *data, struct zext_workspace_handle_v1 *_, + struct wl_array *coordinates) { + std::vector coords_vec; + auto coords = static_cast(coordinates->data); + for (size_t i = 0; i < coordinates->size / sizeof(uint32_t); ++i) { + coords_vec.push_back(coords[i]); + } + + static_cast(data)->handle_coordinates(coords_vec); +} + +void workspace_handle_state(void *data, struct zext_workspace_handle_v1 *workspace_handle, + struct wl_array *state) { + std::vector state_vec; + auto states = static_cast(state->data); + for (size_t i = 0; i < state->size / sizeof(uint32_t); ++i) { + state_vec.push_back(states[i]); + } + + static_cast(data)->handle_state(state_vec); +} + +void workspace_handle_remove(void *data, struct zext_workspace_handle_v1 *_) { + static_cast(data)->handle_remove(); +} + +static const zext_workspace_handle_v1_listener workspace_impl = { + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .remove = workspace_handle_remove}; + +void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data) { + zext_workspace_handle_v1_add_listener(workspace_handle, &workspace_impl, data); +} +} // namespace waybar::modules::wlr diff --git a/src/util/rfkill.cpp b/src/util/rfkill.cpp index df77598b..7400135e 100644 --- a/src/util/rfkill.cpp +++ b/src/util/rfkill.cpp @@ -3,11 +3,11 @@ * Copyright 2009 Johannes Berg * Copyright 2009 Marcel Holtmann * Copyright 2009 Tim Gardner - * + * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. - * + * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR @@ -17,67 +17,66 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "util/rfkill.hpp" -#include -#include -#include -#include + #include -#include +#include +#include +#include +#include + #include -#include -waybar::util::Rfkill::Rfkill(const enum rfkill_type rfkill_type) - : rfkill_type_(rfkill_type) { -} - -void waybar::util::Rfkill::waitForEvent() { - struct rfkill_event event; - struct pollfd p; - ssize_t len; - int fd, n; - - fd = open("/dev/rfkill", O_RDONLY); - if (fd < 0) { - throw std::runtime_error("Can't open RFKILL control device"); +waybar::util::Rfkill::Rfkill(const enum rfkill_type rfkill_type) : rfkill_type_(rfkill_type) { + fd_ = open("/dev/rfkill", O_RDONLY); + if (fd_ < 0) { + spdlog::error("Can't open RFKILL control device"); return; } - - memset(&p, 0, sizeof(p)); - p.fd = fd; - p.events = POLLIN | POLLHUP; - - while (1) { - n = poll(&p, 1, -1); - if (n < 0) { - throw std::runtime_error("Failed to poll RFKILL control device"); - break; - } - - if (n == 0) - continue; - - len = read(fd, &event, sizeof(event)); - if (len < 0) { - throw std::runtime_error("Reading of RFKILL events failed"); - break; - } - - if (len != RFKILL_EVENT_SIZE_V1) { - throw std::runtime_error("Wrong size of RFKILL event"); - continue; - } - - if(event.type == rfkill_type_ && event.op == RFKILL_OP_CHANGE) { - state_ = event.soft || event.hard; - break; - } + int rc = fcntl(fd_, F_SETFL, O_NONBLOCK); + if (rc < 0) { + spdlog::error("Can't set RFKILL control device to non-blocking: {}", errno); + close(fd_); + fd_ = -1; + return; } - - close(fd); - return; + Glib::signal_io().connect( + sigc::mem_fun(*this, &Rfkill::on_event), fd_, Glib::IO_IN | Glib::IO_ERR | Glib::IO_HUP); } - -bool waybar::util::Rfkill::getState() const { - return state_; +waybar::util::Rfkill::~Rfkill() { + if (fd_ >= 0) { + close(fd_); + } } + +bool waybar::util::Rfkill::on_event(Glib::IOCondition cond) { + if (cond & Glib::IO_IN) { + struct rfkill_event event; + ssize_t len; + + len = read(fd_, &event, sizeof(event)); + if (len < 0) { + if (errno == EAGAIN) { + return true; + } + spdlog::error("Reading of RFKILL events failed: {}", errno); + return false; + } + + if (len < RFKILL_EVENT_SIZE_V1) { + spdlog::error("Wrong size of RFKILL event: {} < {}", len, RFKILL_EVENT_SIZE_V1); + return true; + } + + if (event.type == rfkill_type_ && (event.op == RFKILL_OP_ADD || event.op == RFKILL_OP_CHANGE)) { + state_ = event.soft || event.hard; + on_update.emit(event); + } + return true; + } else { + spdlog::error("Failed to poll RFKILL control device"); + return false; + } +} + +bool waybar::util::Rfkill::getState() const { return state_; } diff --git a/src/util/ustring_clen.cpp b/src/util/ustring_clen.cpp new file mode 100644 index 00000000..cd7d9cf5 --- /dev/null +++ b/src/util/ustring_clen.cpp @@ -0,0 +1,9 @@ +#include "util/ustring_clen.hpp" + +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; + } + return total; +} \ No newline at end of file diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap new file mode 100644 index 00000000..c82b310f --- /dev/null +++ b/subprojects/catch2.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = Catch2-2.13.7 +source_url = https://github.com/catchorg/Catch2/archive/v2.13.7.zip +source_filename = Catch2-2.13.7.zip +source_hash = 3f3ccd90ad3a8fbb1beeb15e6db440ccdcbebe378dfd125d07a1f9a587a927e9 +patch_filename = catch2_2.13.7-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/catch2_2.13.7-1/get_patch +patch_hash = 2f7369645d747e5bd866317ac1dd4c3d04dc97d3aad4fc6b864bdf75d3b57158 + +[provide] +catch2 = catch2_dep + diff --git a/subprojects/date.wrap b/subprojects/date.wrap index ea73f0fa..4d4067c9 100644 --- a/subprojects/date.wrap +++ b/subprojects/date.wrap @@ -1,9 +1,9 @@ [wrap-file] -source_url=https://github.com/HowardHinnant/date/archive/v2.4.1.tar.gz -source_filename=date-2.4.1.tar.gz -source_hash=98907d243397483bd7ad889bf6c66746db0d7d2a39cc9aacc041834c40b65b98 -directory=date-2.4.1 +source_url=https://github.com/HowardHinnant/date/archive/v3.0.0.tar.gz +source_filename=date-3.0.0.tar.gz +source_hash=87bba2eaf0ebc7ec539e5e62fc317cb80671a337c1fb1b84cb9e4d42c6dbebe3 +directory=date-3.0.0 -patch_url = https://github.com/mesonbuild/hinnant-date/releases/download/2.4.1-1/hinnant-date.zip -patch_filename = hinnant-date-2.4.1-1-wrap.zip -patch_hash = 2061673a6f8e6d63c3a40df4da58fa2b3de2835fd9b3e74649e8279599f3a8f6 +patch_url = https://github.com/mesonbuild/hinnant-date/releases/download/3.0.0-1/hinnant-date.zip +patch_filename = hinnant-date-3.0.0-1-wrap.zip +patch_hash = 6ccaf70732d8bdbd1b6d5fdf3e1b935c23bf269bda12fdfd0e561276f63432fe diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index eb79283c..71abc80b 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -1,10 +1,13 @@ [wrap-file] -directory = fmt-5.3.0 +directory = fmt-7.1.3 -source_url = https://github.com/fmtlib/fmt/archive/5.3.0.tar.gz -source_filename = fmt-5.3.0.tar.gz -source_hash = defa24a9af4c622a7134076602070b45721a43c51598c8456ec6f2c4dbb51c89 +source_url = https://github.com/fmtlib/fmt/archive/7.1.3.tar.gz +source_filename = fmt-7.1.3.tar.gz +source_hash = 5cae7072042b3043e12d53d50ef404bbb76949dad1de368d7f993a15c8c05ecc -patch_url = https://github.com/mesonbuild/fmt/releases/download/5.3.0-1/fmt.zip -patch_filename = fmt-5.3.0-1-wrap.zip -patch_hash = 18f21a3b8833949c35d4ac88a7059577d5fa24b98786e4b1b2d3d81bb811440f \ No newline at end of file +patch_url = https://github.com/mesonbuild/fmt/releases/download/7.1.3-1/fmt.zip +patch_filename = fmt-7.1.3-1-wrap.zip +patch_hash = 6eb951a51806fd6ffd596064825c39b844c1fe1799840ef507b61a53dba08213 + +[provide] +fmt = fmt_dep diff --git a/subprojects/gtk-layer-shell.wrap b/subprojects/gtk-layer-shell.wrap index b826ab9c..555fbcb6 100644 --- a/subprojects/gtk-layer-shell.wrap +++ b/subprojects/gtk-layer-shell.wrap @@ -1,5 +1,5 @@ [wrap-file] -directory = gtk-layer-shell-0.1.0 -source_filename = gtk-layer-shell-0.1.0.tar.gz -source_hash = f7569e27ae30b1a94c3ad6c955cf56240d6bc272b760d9d266ce2ccdb94a5cf0 -source_url = https://github.com/wmww/gtk-layer-shell/archive/v0.1.0/gtk-layer-shell-0.1.0.tar.gz +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 diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap index 750036b9..daddfd61 100644 --- a/subprojects/spdlog.wrap +++ b/subprojects/spdlog.wrap @@ -1,10 +1,11 @@ [wrap-file] -directory = spdlog-1.3.1 +directory = spdlog-1.8.5 +source_url = https://github.com/gabime/spdlog/archive/v1.8.5.tar.gz +source_filename = v1.8.5.tar.gz +source_hash = 944d0bd7c763ac721398dca2bb0f3b5ed16f67cef36810ede5061f35a543b4b8 +patch_url = https://wrapdb.mesonbuild.com/v1/projects/spdlog/1.8.5/1/get_zip +patch_filename = spdlog-1.8.5-1-wrap.zip +patch_hash = 3c38f275d5792b1286391102594329e98b17737924b344f98312ab09929b74be -source_url = https://github.com/gabime/spdlog/archive/v1.3.1.tar.gz -source_filename = v1.3.1.tar.gz -source_hash = 160845266e94db1d4922ef755637f6901266731c4cb3b30b45bf41efa0e6ab70 - -patch_url = https://github.com/mesonbuild/spdlog/releases/download/1.3.1-1/spdlog.zip -patch_filename = spdlog-1.3.1-1-wrap.zip -patch_hash = 715a0229781019b853d409cc0bf891ee4b9d3a17bec0cf87f4ad30b28bbecc87 +[provide] +spdlog = spdlog_dep diff --git a/test/config.cpp b/test/config.cpp new file mode 100644 index 00000000..edd6d6b8 --- /dev/null +++ b/test/config.cpp @@ -0,0 +1,115 @@ +#define CATCH_CONFIG_MAIN +#include "config.hpp" + +#include + +TEST_CASE("Load simple config", "[config]") { + waybar::Config conf; + conf.load("test/config/simple.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load config with multiple bars", "[config]") { + waybar::Config conf; + conf.load("test/config/multi.json"); + + SECTION("select multiple configs #1") { + auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0"); + REQUIRE(data.size() == 3); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "top"); + REQUIRE(data[1]["position"].asString() == "bottom"); + REQUIRE(data[1]["height"].asInt() == 21); + REQUIRE(data[2]["layer"].asString() == "overlay"); + REQUIRE(data[2]["position"].asString() == "right"); + REQUIRE(data[2]["height"].asInt() == 23); + } + SECTION("select multiple configs #2") { + auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(data.size() == 2); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "overlay"); + REQUIRE(data[1]["position"].asString() == "right"); + REQUIRE(data[1]["height"].asInt() == 23); + } + SECTION("select single config by output description") { + auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["layer"].asString() == "overlay"); + REQUIRE(data[0]["position"].asString() == "left"); + REQUIRE(data[0]["height"].asInt() == 22); + } +} + +TEST_CASE("Load simple config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + // config override behavior: preserve first included value + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + // config override behavior: preserve value from the top config + REQUIRE(data["position"].asString() == "top"); + // config override behavior: explicit null is still a value and should be preserved + REQUIRE((data.isMember("nullOption") && data["nullOption"].isNull())); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load multiple bar config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include-multi.json"); + + SECTION("bar config with sole include") { + auto data = conf.getOutputConfigs("OUT-0", "Fake output #0"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 20); + } + + SECTION("bar config with output and include") { + auto data = conf.getOutputConfigs("OUT-1", "Fake output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 21); + } + + SECTION("bar config with output override") { + auto data = conf.getOutputConfigs("OUT-2", "Fake output #2"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 22); + } + + SECTION("multiple levels of include") { + auto data = conf.getOutputConfigs("OUT-3", "Fake output #3"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 23); + } + + auto& data = conf.getConfig(); + REQUIRE(data.isArray()); + REQUIRE(data.size() == 4); + REQUIRE(data[0]["output"].asString() == "OUT-0"); +} diff --git a/test/config/include-1.json b/test/config/include-1.json new file mode 100644 index 00000000..7c47a882 --- /dev/null +++ b/test/config/include-1.json @@ -0,0 +1,7 @@ +{ + "layer": "top", + "position": "bottom", + "height": 30, + "output": ["HDMI-0", "DP-0"], + "nullOption": "not null" +} diff --git a/test/config/include-2.json b/test/config/include-2.json new file mode 100644 index 00000000..741194f1 --- /dev/null +++ b/test/config/include-2.json @@ -0,0 +1,3 @@ +{ + "layer": "bottom" +} diff --git a/test/config/include-multi-0.json b/test/config/include-multi-0.json new file mode 100644 index 00000000..a4c3fc17 --- /dev/null +++ b/test/config/include-multi-0.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-0", + "height": 20 +} diff --git a/test/config/include-multi-1.json b/test/config/include-multi-1.json new file mode 100644 index 00000000..2b28d6c5 --- /dev/null +++ b/test/config/include-multi-1.json @@ -0,0 +1,3 @@ +{ + "height": 21 +} diff --git a/test/config/include-multi-2.json b/test/config/include-multi-2.json new file mode 100644 index 00000000..f74c2b4e --- /dev/null +++ b/test/config/include-multi-2.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-1", + "height": 22 +} diff --git a/test/config/include-multi-3-0.json b/test/config/include-multi-3-0.json new file mode 100644 index 00000000..11cdd3f9 --- /dev/null +++ b/test/config/include-multi-3-0.json @@ -0,0 +1,3 @@ +{ + "height": 23 +} diff --git a/test/config/include-multi-3.json b/test/config/include-multi-3.json new file mode 100644 index 00000000..309fe15e --- /dev/null +++ b/test/config/include-multi-3.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-3", + "include": "test/config/include-multi-3-0.json" +} diff --git a/test/config/include-multi.json b/test/config/include-multi.json new file mode 100644 index 00000000..e128aba5 --- /dev/null +++ b/test/config/include-multi.json @@ -0,0 +1,16 @@ +[ + { + "include": "test/config/include-multi-0.json" + }, + { + "output": "OUT-1", + "include": "test/config/include-multi-1.json" + }, + { + "output": "OUT-2", + "include": "test/config/include-multi-2.json" + }, + { + "include": "test/config/include-multi-3.json" + } +] diff --git a/test/config/include.json b/test/config/include.json new file mode 100644 index 00000000..c46aaf24 --- /dev/null +++ b/test/config/include.json @@ -0,0 +1,5 @@ +{ + "include": ["test/config/include-1.json", "test/config/include-2.json"], + "position": "top", + "nullOption": null +} diff --git a/test/config/multi.json b/test/config/multi.json new file mode 100644 index 00000000..ed43a39e --- /dev/null +++ b/test/config/multi.json @@ -0,0 +1,25 @@ +[ + { + "layer": "bottom", + "height": 20, + "output": ["HDMI-0", "DP-0"] + }, + { + "position": "bottom", + "layer": "top", + "height": 21, + "output": ["DP-0"] + }, + { + "position": "left", + "layer": "overlay", + "height": 22, + "output": "Fake HDMI output #1" + }, + { + "position": "right", + "layer": "overlay", + "height": 23, + "output": "!HDMI-1" + } +] diff --git a/test/config/simple.json b/test/config/simple.json new file mode 100644 index 00000000..1cb1e3a1 --- /dev/null +++ b/test/config/simple.json @@ -0,0 +1,5 @@ +{ + "layer": "top", + "height": 30, + "output": ["HDMI-0", "DP-0"] +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 00000000..85b9771f --- /dev/null +++ b/test/meson.build @@ -0,0 +1,21 @@ +test_inc = include_directories('../include') +test_dep = [ + catch2, + fmt, + jsoncpp, + spdlog, +] + +config_test = executable( + 'config_test', + 'config.cpp', + '../src/config.cpp', + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'Configuration test', + config_test, + workdir: meson.source_root(), +)