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 [](https://travis-ci.org/Alexays/Waybar) [](LICENSE) [](https://paypal.me/ARouillard)

+# Waybar [](LICENSE) [](https://paypal.me/ARouillard)

 
 > 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:
+
+[](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