diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 4a774dbd..0fad47c4 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -7,14 +7,16 @@ concurrency: cancel-in-progress: true jobs: - build: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: DoozyX/clang-format-lint-action@v0.16.2 + # TODO: bump to clang 19 release + # - uses: DoozyX/clang-format-lint-action@v0.18.2 + - uses: DoozyX/clang-format-lint-action@558090054b3f39e3d6af24f0cd73b319535da809 name: clang-format with: source: "." extensions: "hpp,h,cpp,c" style: "file:.clang-format" - clangFormatVersion: 18 + clangFormatVersion: 19 diff --git a/.github/workflows/clang-tidy.yml.bak b/.github/workflows/clang-tidy.yml.bak index a39bd23d..ec67fb7e 100644 --- a/.github/workflows/clang-tidy.yml.bak +++ b/.github/workflows/clang-tidy.yml.bak @@ -7,7 +7,7 @@ concurrency: cancel-in-progress: true jobs: - build: + lint: runs-on: ubuntu-latest container: image: alexays/waybar:debian diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 66c465ba..0e7e2944 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,14 +1,15 @@ name: Build and Push Docker Image on: + workflow_dispatch: schedule: - # run every night at midnight - - cron: '0 0 * * *' + # run monthly + - cron: '0 0 1 * *' jobs: build-and-push: runs-on: ubuntu-latest - if: github.repository == 'Alexays/Waybar' + if: github.event_name != 'schedule' || github.repository == 'Alexays/Waybar' strategy: fail-fast: false # don't fail the other jobs if one of the images fails to build matrix: diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index bbb97198..b9114c31 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -7,21 +7,21 @@ concurrency: cancel-in-progress: true jobs: - clang: + build: # Run actions in a FreeBSD VM on the ubuntu runner # https://github.com/actions/runner/issues/385 - for FreeBSD runner support runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Test in FreeBSD VM - uses: cross-platform-actions/action@v0.25.0 + uses: cross-platform-actions/action@v0.28.0 timeout-minutes: 180 env: CPPFLAGS: '-isystem/usr/local/include' LDFLAGS: '-L/usr/local/lib' with: operating_system: freebsd - version: "14.1" + version: "14.2" environment_variables: CPPFLAGS LDFLAGS sync_files: runner-to-vm run: | diff --git a/Dockerfiles/debian b/Dockerfiles/debian index f479062d..c2584ccf 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -1,6 +1,6 @@ # vim: ft=Dockerfile -FROM debian:sid +FROM debian:sid-slim RUN apt update && \ apt install --no-install-recommends --no-install-suggests -y \ diff --git a/Dockerfiles/gentoo b/Dockerfiles/gentoo index f2ec0dc9..f7023825 100644 --- a/Dockerfiles/gentoo +++ b/Dockerfiles/gentoo @@ -6,6 +6,6 @@ RUN export FEATURES="-ipc-sandbox -network-sandbox -pid-sandbox -sandbox -usersa emerge --sync && \ eselect news read --quiet new 1>/dev/null 2>&1 && \ emerge --verbose --update --deep --with-bdeps=y --backtrack=30 --newuse @world && \ - USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols =dev-cpp/gtkmm-3.24.6 x11-libs/libxkbcommon \ + USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols dev-cpp/gtkmm:3.0 x11-libs/libxkbcommon \ x11-libs/gtk+:3 dev-libs/libdbusmenu dev-libs/libnl sys-power/upower media-libs/libpulse dev-libs/libevdev media-libs/libmpdclient \ media-sound/sndio gui-libs/gtk-layer-shell app-text/scdoc media-sound/playerctl dev-libs/iniparser sci-libs/fftw diff --git a/LICENSE b/LICENSE index 41eb81d8..d1bad1b4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Alex +Copyright (c) 2025 Alex Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 55a6c7d9..5266e916 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - River (Mapping mode, Tags, Focused window name) - Hyprland (Window Icons, Workspaces, Focused window name) - Niri (Workspaces, Focused window name, Language) -- DWL (Tags, Focused window name) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) +- DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery diff --git a/default.nix b/default.nix index 2cccff28..6466507b 100644 --- a/default.nix +++ b/default.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index 4b3e7212..5c08338e 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727634051, - "narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=", + "lastModified": 1751011381, + "narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9", + "rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 571c4934..7c7a2281 100644 --- a/flake.nix +++ b/flake.nix @@ -9,47 +9,96 @@ }; }; - outputs = { self, nixpkgs, ... }: + outputs = + { self, nixpkgs, ... }: let inherit (nixpkgs) lib; - genSystems = func: lib.genAttrs [ - "x86_64-linux" - "aarch64-linux" - ] - (system: func (import nixpkgs { - inherit system; - overlays = with self.overlays; [ - waybar - ]; - })); + genSystems = + func: + lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + ] + ( + system: + func ( + import nixpkgs { + inherit system; + overlays = with self.overlays; [ + waybar + ]; + } + ) + ); - mkDate = longDate: (lib.concatStringsSep "-" [ - (builtins.substring 0 4 longDate) - (builtins.substring 4 2 longDate) - (builtins.substring 6 2 longDate) - ]); + mkDate = + longDate: + (lib.concatStringsSep "-" [ + (builtins.substring 0 4 longDate) + (builtins.substring 4 2 longDate) + (builtins.substring 6 2 longDate) + ]); in { - devShells = genSystems - (pkgs: - { - default = - pkgs.mkShell - { - name = "waybar-shell"; + devShells = genSystems (pkgs: { + default = pkgs.mkShell { + name = "waybar-shell"; - # inherit attributes from upstream nixpkgs derivation - inherit (pkgs.waybar) buildInputs depsBuildBuild depsBuildBuildPropagated depsBuildTarget - depsBuildTargetPropagated depsHostHost depsHostHostPropagated depsTargetTarget - depsTargetTargetPropagated propagatedBuildInputs propagatedNativeBuildInputs strictDeps; + # inherit attributes from upstream nixpkgs derivation + inherit (pkgs.waybar) + buildInputs + depsBuildBuild + depsBuildBuildPropagated + depsBuildTarget + depsBuildTargetPropagated + depsHostHost + depsHostHostPropagated + depsTargetTarget + depsTargetTargetPropagated + propagatedBuildInputs + propagatedNativeBuildInputs + strictDeps + ; - # overrides for local development - nativeBuildInputs = pkgs.waybar.nativeBuildInputs ++ (with pkgs; [ - clang-tools - gdb - ]); + # overrides for local development + nativeBuildInputs = + pkgs.waybar.nativeBuildInputs + ++ (with pkgs; [ + nixfmt-rfc-style + clang-tools + gdb + ]); + }; + }); + + formatter = genSystems ( + pkgs: + pkgs.treefmt.withConfig { + settings = [ + { + formatter = { + clang-format = { + options = [ "-i" ]; + command = lib.getExe' pkgs.clang-tools "clang-format"; + excludes = [ ]; + includes = [ + "*.c" + "*.cpp" + "*.h" + "*.hpp" + ]; }; - }); + nixfmt = { + command = lib.getExe pkgs.nixfmt-rfc-style; + includes = [ "*.nix" ]; + }; + }; + tree-root-file = ".git/index"; + } + ]; + } + ); overlays = { default = self.overlays.waybar; @@ -58,11 +107,15 @@ waybar = prev.waybar; # take the first "version: '...'" from meson.build version = - (builtins.head (builtins.split "'" - (builtins.elemAt - (builtins.split " version: '" (builtins.readFile ./meson.build)) - 2))) - + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); + (builtins.head ( + builtins.split "'" ( + builtins.elemAt (builtins.split " version: '" (builtins.readFile ./meson.build)) 2 + ) + )) + + "+date=" + + (mkDate (self.lastModifiedDate or "19700101")) + + "_" + + (self.shortRev or "dirty"); }; }; }; diff --git a/include/AModule.hpp b/include/AModule.hpp index 94a88371..2fcbfc23 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -23,6 +23,8 @@ class AModule : public IModule { /// Emitting on this dispatcher triggers a update() call Glib::Dispatcher dp; + bool expandEnabled() const; + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -34,6 +36,7 @@ class AModule : public IModule { SCROLL_DIR getScrollDir(GdkEventScroll *e); bool tooltipEnabled() const; + std::vector pid_children_; const std::string name_; const Json::Value &config_; Gtk::EventBox event_box_; @@ -50,8 +53,8 @@ class AModule : public IModule { private: bool handleUserEvent(GdkEventButton *const &ev); const bool isTooltip; + const bool isExpand; bool hasUserEvents_; - std::vector pid_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; std::map eventActionMap_; diff --git a/include/config.hpp b/include/config.hpp index 18a1daed..5256bb46 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -20,8 +20,8 @@ class Config { static std::optional findConfigPath( const std::vector &names, const std::vector &dirs = CONFIG_DIRS); - static std::optional tryExpandPath(const std::string &base, - const std::string &filename); + static std::vector tryExpandPath(const std::string &base, + const std::string &filename); Config() = default; diff --git a/include/modules/cava.hpp b/include/modules/cava.hpp index 219d9302..1a88c7b7 100644 --- a/include/modules/cava.hpp +++ b/include/modules/cava.hpp @@ -5,7 +5,16 @@ namespace cava { extern "C" { +// Need sdl_glsl output feature to be enabled on libcava +#ifndef SDL_GLSL +#define SDL_GLSL +#endif + #include + +#ifdef SDL_GLSL +#undef SDL_GLSL +#endif } } // namespace cava @@ -23,11 +32,11 @@ class Cava final : public ALabel { util::SleeperThread thread_; util::SleeperThread thread_fetch_input_; - struct cava::error_s error_ {}; // cava errors - struct cava::config_params prm_ {}; // cava parameters - struct cava::audio_raw audio_raw_ {}; // cava handled raw audio data(is based on audio_data) - struct cava::audio_data audio_data_ {}; // cava audio data - struct cava::cava_plan* plan_; //{new cava_plan{}}; + struct cava::error_s error_{}; // cava errors + struct cava::config_params prm_{}; // cava parameters + struct cava::audio_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data) + struct cava::audio_data audio_data_{}; // cava audio data + struct cava::cava_plan* plan_; //{new cava_plan{}}; // Cava API to read audio source cava::ptr input_source_; // Delay to handle audio source @@ -44,7 +53,7 @@ class Cava final : public ALabel { // Cava method void pause_resume(); // ModuleActionMap - static inline std::map actionMap_{ + static inline std::map actionMap_{ {"mode", &waybar::modules::Cava::pause_resume}}; }; } // namespace waybar::modules diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 0c62b676..e34b7a8e 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -38,39 +38,39 @@ class Clock final : public ALabel { 5 - tooltip-format */ std::map fmtMap_; - uint cldMonCols_{3}; // calendar count month columns - int cldWnLen_{3}; // calendar week number length - const int cldMonColLen_{20}; // calendar month column length - WS cldWPos_{WS::HIDDEN}; // calendar week side to print - months cldCurrShift_{0}; // calendar months shift - int cldShift_{1}; // calendar months shift factor - year_month_day cldYearShift_; // calendar Year mode. Cached ymd - std::string cldYearCached_; // calendar Year mode. Cached calendar - year_month cldMonShift_; // calendar Month mode. Cached ym - std::string cldMonCached_; // calendar Month mode. Cached calendar - day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) - std::string cldText_{""}; // calendar text to print + uint cldMonCols_{3}; // calendar count month columns + int cldWnLen_{3}; // calendar week number length + const int cldMonColLen_{20}; // calendar month column length + WS cldWPos_{WS::HIDDEN}; // calendar week side to print + date::months cldCurrShift_{0}; // calendar months shift + int cldShift_{1}; // calendar months shift factor + date::year_month_day cldYearShift_; // calendar Year mode. Cached ymd + std::string cldYearCached_; // calendar Year mode. Cached calendar + date::year_month cldMonShift_; // calendar Month mode. Cached ym + std::string cldMonCached_; // calendar Month mode. Cached calendar + date::day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) + std::string cldText_{""}; // calendar text to print CldMode cldMode_{CldMode::MONTH}; - auto get_calendar(const year_month_day& today, const year_month_day& ymd, - const time_zone* tz) -> const std::string; + auto get_calendar(const date::year_month_day& today, const date::year_month_day& ymd, + const date::time_zone* tz) -> const std::string; // get local time zone - auto local_zone() -> const time_zone*; + auto local_zone() -> const date::time_zone*; // time zoned time in tooltip - const bool tzInTooltip_; // if need to print time zones text - std::vector tzList_; // time zones list - int tzCurrIdx_; // current time zone index for tzList_ - std::string tzText_{""}; // time zones text to print + const bool tzInTooltip_; // if need to print time zones text + std::vector tzList_; // time zones list + int tzCurrIdx_; // current time zone index for tzList_ + std::string tzText_{""}; // time zones text to print util::SleeperThread thread_; // ordinal date in tooltip const bool ordInTooltip_; std::string ordText_{""}; - auto get_ordinal_date(const year_month_day& today) -> std::string; + auto get_ordinal_date(const date::year_month_day& today) -> std::string; - auto getTZtext(sys_seconds now) -> std::string; - auto first_day_of_week() -> weekday; + auto getTZtext(date::sys_seconds now) -> std::string; + auto first_day_of_week() -> date::weekday; // Module actions void cldModeSwitch(); void cldShift_up(); @@ -79,7 +79,7 @@ class Clock final : public ALabel { void tz_up(); void tz_down(); // Module Action Map - static inline std::map actionMap_{ + static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, {"shift_up", &waybar::modules::Clock::cldShift_up}, {"shift_down", &waybar::modules::Clock::cldShift_down}, diff --git a/include/modules/gps.hpp b/include/modules/gps.hpp new file mode 100644 index 00000000..80df12ba --- /dev/null +++ b/include/modules/gps.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#ifdef WANT_RFKILL +#include "util/rfkill.hpp" +#endif + +#include + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Gps : public ALabel { + public: + Gps(const std::string&, const Json::Value&); + virtual ~Gps(); + auto update() -> void override; + + private: +#ifdef WANT_RFKILL + util::Rfkill rfkill_; +#endif + const std::string getFixModeName() const; + const std::string getFixModeString() const; + + const std::string getFixStatusString() const; + + util::SleeperThread thread_, gps_thread_; + gps_data_t gps_data_; + std::string state_; + + bool hideDisconnected = true; + bool hideNoFix = false; +}; + +} // namespace waybar::modules diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index 11e73d8f..d9f16526 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include #include #include "util/json.hpp" @@ -19,7 +19,9 @@ class EventHandler { class IPC { public: - IPC() { startIPC(); } + IPC(); + ~IPC(); + static IPC& inst(); void registerForIPC(const std::string& ev, EventHandler* ev_handler); void unregisterForIPC(EventHandler* handler); @@ -32,14 +34,17 @@ class IPC { static std::filesystem::path socketFolder_; private: - void startIPC(); + void socketListener(); void parseIPC(const std::string&); + std::thread ipcThread_; std::mutex callbackMutex_; util::JsonParser parser_; std::list> callbacks_; + int socketfd_; // the hyprland socket file descriptor + bool running_ = true; }; -inline std::unique_ptr gIPC; inline bool modulesReady = false; +inline std::unique_ptr gIPC; }; // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 47a4d69c..ec59e5c3 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -37,6 +37,8 @@ class Language : public waybar::ALabel, public EventHandler { util::JsonParser parser_; Layout layout_; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp index ce980f36..7e3425ef 100644 --- a/include/modules/hyprland/submap.hpp +++ b/include/modules/hyprland/submap.hpp @@ -28,6 +28,8 @@ class Submap : public waybar::ALabel, public EventHandler { std::string submap_; bool always_on_ = false; std::string default_submap_ = "Default"; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index f2c266bd..2be64594 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -60,6 +60,8 @@ class Window : public waybar::AAppIconLabel, public EventHandler { bool swallowing_; bool fullscreen_; bool focused_; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcount.hpp b/include/modules/hyprland/windowcount.hpp new file mode 100644 index 00000000..195e6a34 --- /dev/null +++ b/include/modules/hyprland/windowcount.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/json.hpp" + +namespace waybar::modules::hyprland { + +class WindowCount : public waybar::AAppIconLabel, public EventHandler { + public: + WindowCount(const std::string&, const waybar::Bar&, const Json::Value&); + ~WindowCount() override; + + auto update() -> void override; + + private: + struct Workspace { + int id; + int windows; + bool hasfullscreen; + static auto parse(const Json::Value& value) -> Workspace; + }; + + static auto getActiveWorkspace(const std::string&) -> Workspace; + static auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string& ev) override; + void queryActiveWorkspace(); + void setClass(const std::string&, bool enable); + + bool separateOutputs_; + std::mutex mutex_; + const Bar& bar_; + Workspace workspace_; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp index 45229ed4..e4180ed9 100644 --- a/include/modules/hyprland/windowcreationpayload.hpp +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -42,7 +42,7 @@ class WindowCreationPayload { std::string getWorkspaceName() const { return m_workspaceName; } WindowAddress getAddress() const { return m_windowAddress; } - void moveToWorksace(std::string& new_workspace_name); + void moveToWorkspace(std::string& new_workspace_name); private: void clearAddr(); diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index f1fea4e8..a257ddef 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -55,11 +55,11 @@ class Workspace { void setName(std::string const& value) { m_name = value; }; void setOutput(std::string const& value) { m_output = value; }; bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } - void insertWindow(WindowCreationPayload create_window_paylod); + void insertWindow(WindowCreationPayload create_window_payload); std::string removeWindow(WindowAddress const& addr); void initializeWindowMap(const Json::Value& clients_data); - bool onWindowOpened(WindowCreationPayload const& create_window_paylod); + bool onWindowOpened(WindowCreationPayload const& create_window_payload); std::optional closeWindow(WindowAddress const& addr); void update(const std::string& format, const std::string& icon); @@ -83,6 +83,7 @@ class Workspace { Gtk::Button m_button; Gtk::Box m_content; Gtk::Label m_label; + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index a9d56b79..9a44e594 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ class Workspaces : public AModule, public EventHandler { auto showSpecial() const -> bool { return m_showSpecial; } auto activeOnly() const -> bool { return m_activeOnly; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } + auto persistentOnly() const -> bool { return m_persistentOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } auto getBarOutput() const -> std::string { return m_bar.output->name; } @@ -49,20 +51,21 @@ class Workspaces : public AModule, public EventHandler { private: void onEvent(const std::string& e) override; void updateWindowCount(); + void sortSpecialCentered(); void sortWorkspaces(); void createWorkspace(Json::Value const& workspace_data, Json::Value const& clients_data = Json::Value::nullRef); static Json::Value createMonitorWorkspaceData(std::string const& name, std::string const& monitor); - void removeWorkspace(std::string const& name); + void removeWorkspace(std::string const& workspaceString); void setUrgentWorkspace(std::string const& windowaddress); // Config void parseConfig(const Json::Value& config); auto populateIconsMap(const Json::Value& formatIcons) -> void; - static auto populateBoolConfig(const Json::Value& config, const std::string& key, - bool& member) -> void; + static auto populateBoolConfig(const Json::Value& config, const std::string& key, bool& member) + -> void; auto populateSortByConfig(const Json::Value& config) -> void; auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; @@ -74,10 +77,11 @@ class Workspaces : public AModule, public EventHandler { void onWorkspaceActivated(std::string const& payload); void onSpecialWorkspaceActivated(std::string const& payload); void onWorkspaceDestroyed(std::string const& payload); - void onWorkspaceCreated(std::string const& workspaceName, + void onWorkspaceCreated(std::string const& payload, Json::Value const& clientsData = Json::Value::nullRef); void onWorkspaceMoved(std::string const& payload); void onWorkspaceRenamed(std::string const& payload); + static std::optional parseWorkspaceId(std::string const& workspaceIdStr); // monitor events void onMonitorFocused(std::string const& payload); @@ -93,11 +97,18 @@ class Workspaces : public AModule, public EventHandler { int windowRewritePriorityFunction(std::string const& window_rule); + // event payload management + template + static std::string makePayload(Args const&... args); + static std::pair splitDoublePayload(std::string const& payload); + static std::tuple splitTriplePayload( + std::string const& payload); + // Update methods void doUpdate(); void removeWorkspacesToRemove(); void createWorkspacesToCreate(); - static std::vector getVisibleWorkspaces(); + static std::vector getVisibleWorkspaces(); void updateWorkspaceStates(); bool updateWindowsToCreate(); @@ -113,20 +124,22 @@ class Workspaces : public AModule, public EventHandler { bool m_showSpecial = false; bool m_activeOnly = false; bool m_specialVisibleOnly = false; + bool m_persistentOnly = false; bool m_moveToMonitor = false; Json::Value m_persistentWorkspaceConfig; // Map for windows stored in workspaces not present in the current bar. // This happens when the user has multiple monitors (hence, multiple bars) - // and doesn't share windows accross bars (a.k.a `all-outputs` = false) + // and doesn't share windows across bars (a.k.a `all-outputs` = false) std::map m_orphanWindowMap; - enum class SortMethod { ID, NAME, NUMBER, DEFAULT }; + enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT }; util::EnumParser m_enumParser; SortMethod m_sortBy = SortMethod::DEFAULT; std::map m_sortMap = {{"ID", SortMethod::ID}, {"NAME", SortMethod::NAME}, {"NUMBER", SortMethod::NUMBER}, + {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, {"DEFAULT", SortMethod::DEFAULT}}; std::string m_format; @@ -138,7 +151,7 @@ class Workspaces : public AModule, public EventHandler { bool m_withIcon; uint64_t m_monitorId; - std::string m_activeWorkspaceName; + int m_activeWorkspaceId; std::string m_activeSpecialWorkspaceName; std::vector> m_workspaces; std::vector> m_workspacesToCreate; @@ -150,6 +163,7 @@ class Workspaces : public AModule, public EventHandler { std::mutex m_mutex; const Bar& m_bar; Gtk::Box m_box; + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 4a84b02f..5fd0c180 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -16,6 +16,8 @@ #include "util/rfkill.hpp" #endif +enum ip_addr_pref : uint8_t { IPV4, IPV6, IPV4_6 }; + namespace waybar::modules { class Network : public ALabel { @@ -50,7 +52,7 @@ class Network : public ALabel { std::optional> readBandwidthUsage(); int ifid_; - sa_family_t family_; + ip_addr_pref addr_pref_; struct sockaddr_nl nladdr_ = {0}; struct nl_sock* sock_ = nullptr; struct nl_sock* ev_sock_ = nullptr; @@ -74,9 +76,12 @@ class Network : public ALabel { bool carrier_; std::string ifname_; std::string ipaddr_; + std::string ipaddr6_; std::string gwaddr_; std::string netmask_; + std::string netmask6_; int cidr_; + int cidr6_; int32_t signal_strength_dbm_; uint8_t signal_strength_; std::string signal_strength_app_; diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index d7656d31..cb6a34da 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -13,7 +13,7 @@ namespace waybar::modules::privacy { class Privacy : public AModule { public: - Privacy(const std::string &, const Json::Value &, const std::string &pos); + Privacy(const std::string &, const Json::Value &, Gtk::Orientation, const std::string &pos); auto update() -> void override; void onPrivacyNodesChanged(); @@ -31,6 +31,8 @@ class Privacy : public AModule { uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; + std::set> ignore; + bool ignore_monitor = true; std::shared_ptr backend = nullptr; }; diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 836bd994..f5f572c0 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -17,8 +17,8 @@ namespace waybar::modules::privacy { class PrivacyItem : public Gtk::Revealer { public: PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, - std::list *nodes, const std::string &pos, const uint icon_size, - const uint transition_duration); + std::list *nodes, Gtk::Orientation orientation, + const std::string &pos, const uint icon_size, const uint transition_duration); enum PrivacyNodeType privacy_type; diff --git a/include/modules/sni/icon_manager.hpp b/include/modules/sni/icon_manager.hpp new file mode 100644 index 00000000..614d42d9 --- /dev/null +++ b/include/modules/sni/icon_manager.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include +#include + +class IconManager { + public: + static IconManager& instance() { + static IconManager instance; + return instance; + } + + void setIconsConfig(const Json::Value& icons_config) { + if (icons_config.isObject()) { + for (const auto& key : icons_config.getMemberNames()) { + std::string app_name = key; + const Json::Value& icon_value = icons_config[key]; + + if (icon_value.isString()) { + std::string icon_path = icon_value.asString(); + icons_map_[app_name] = icon_path; + } + } + } else { + spdlog::warn("Invalid icon config format."); + } + } + + std::string getIconForApp(const std::string& app_name) const { + auto it = icons_map_.find(app_name); + if (it != icons_map_.end()) { + return it->second; + } + return ""; + } + + private: + IconManager() = default; + std::unordered_map icons_map_; +}; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index ebc08d45..c5e86d37 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -62,6 +62,7 @@ class Item : public sigc::trackable { void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); void setStatus(const Glib::ustring& value); + void setCustomIcon(const std::string& id); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); void onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index ea58c4f0..91aa181d 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -21,7 +21,7 @@ class Language : public ALabel, public sigc::trackable { auto update() -> void override; private: - enum class DispayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; + enum class DisplayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; struct Layout { std::string full_name; @@ -58,7 +58,7 @@ class Language : public ALabel, public sigc::trackable { std::map layouts_map_; bool hide_single_; bool is_variant_displayed; - std::byte displayed_short_flag = static_cast(DispayedShortFlag::None); + std::byte displayed_short_flag = static_cast(DisplayedShortFlag::None); util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index 427c2e81..60cd2679 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -19,10 +19,11 @@ class Window : public AAppIconLabel, public sigc::trackable { auto update() -> void override; private: - void setClass(std::string classname, bool enable); + void setClass(const std::string& classname, bool enable); void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - std::tuple + std::tuple getFocusedNode(const Json::Value& nodes, std::string& output); void getTree(); @@ -35,6 +36,7 @@ class Window : public AAppIconLabel, public sigc::trackable { std::string old_app_id_; std::size_t app_nb_; std::string shell_; + std::string marks_; int floating_count_; util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 97f4e950..d8a9e18a 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -48,8 +48,7 @@ class Workspaces : public AModule, public sigc::trackable { std::vector high_priority_named_; std::vector workspaces_order_; Gtk::Box box_; - std::string m_formatWindowSeperator; - std::string m_windowRewriteDefault; + std::string m_formatWindowSeparator; util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp index 9c3fbcee..48b0074e 100644 --- a/include/modules/systemd_failed_units.hpp +++ b/include/modules/systemd_failed_units.hpp @@ -19,12 +19,15 @@ class SystemdFailedUnits : public ALabel { std::string format_ok; bool update_pending; - uint32_t nr_failed_system, nr_failed_user; + std::string system_state, user_state, overall_state; + uint32_t nr_failed_system, nr_failed_user, nr_failed; std::string last_status; Glib::RefPtr system_proxy, user_proxy; void notify_cb(const Glib::ustring &sender_name, const Glib::ustring &signal_name, const Glib::VariantContainerBase &arguments); + void RequestFailedUnits(); + void RequestSystemState(); void updateData(); }; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 5440df77..918281be 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -18,6 +18,7 @@ class Temperature : public ALabel { private: float getTemperature(); bool isCritical(uint16_t); + bool isWarning(uint16_t); std::string file_path_; util::SleeperThread thread_; diff --git a/include/modules/wayfire/backend.hpp b/include/modules/wayfire/backend.hpp new file mode 100644 index 00000000..9d55c820 --- /dev/null +++ b/include/modules/wayfire/backend.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace waybar::modules::wayfire { + +using EventHandler = std::function; + +struct State { + /* + ┌───────────┐ ┌───────────┐ + │ output #1 │ │ output #2 │ + └─────┬─────┘ └─────┬─────┘ + └─┐ └─────┐─ ─ ─ ─ ─ ─ ─ ─ ┐ + ┌───────┴───────┐ ┌───────┴──────┐ ┌───────┴───────┐ + │ wset #1 │ │ wset #2 │ │ wset #3 │ + │┌────────────┐ │ │┌────────────┐│ │┌────────────┐ │ + ││ workspaces │ │ ││ workspaces ││ ││ workspaces │ │ + │└─┬──────────┘ │ │└────────────┘│ │└─┬──────────┘ │ + │ │ ┌─────────┐│ └──────────────┘ │ │ ┌─────────┐│ + │ ├─┤ view #1 ││ │ └─┤ view #3 ││ + │ │ └─────────┘│ │ └─────────┘│ + │ │ ┌─────────┐│ └───────────────┘ + │ └─┤ view #2 ││ + │ └─────────┘│ + └───────────────┘ + */ + + struct Output { + size_t id; + size_t w, h; + size_t wset_idx; + }; + + struct Workspace { + size_t num_views; + size_t num_sticky_views; + }; + + struct Wset { + std::optional> output; + std::vector wss; + size_t ws_w, ws_h, ws_x, ws_y; + size_t focused_view_id; + + auto ws_idx() const { return ws_w * ws_y + ws_x; } + auto count_ws(const Json::Value& pos) -> Workspace&; + auto locate_ws(const Json::Value& geo) -> Workspace&; + auto locate_ws(const Json::Value& geo) const -> const Workspace&; + }; + + std::unordered_map outputs; + std::unordered_map wsets; + std::unordered_map views; + std::string focused_output_name; + size_t maybe_empty_focus_wset_idx = {}; + size_t vswitch_sticky_view_id = {}; + bool new_output_detected = {}; + bool vswitching = {}; + + auto update_view(const Json::Value& view) -> void; +}; + +struct Sock { + int fd; + + Sock(int fd) : fd{fd} {} + ~Sock() { close(fd); } + Sock(const Sock&) = delete; + auto operator=(const Sock&) = delete; + Sock(Sock&& rhs) noexcept { + fd = rhs.fd; + rhs.fd = -1; + } + auto& operator=(Sock&& rhs) noexcept { + fd = rhs.fd; + rhs.fd = -1; + return *this; + } +}; + +class IPC { + static std::weak_ptr instance; + Json::CharReaderBuilder reader_builder; + Json::StreamWriterBuilder writer_builder; + std::list>> handlers; + std::mutex handlers_mutex; + State state; + std::mutex state_mutex; + + IPC() { start(); } + + static auto connect() -> Sock; + auto receive(Sock& sock) -> Json::Value; + auto start() -> void; + auto root_event_handler(const std::string& event, const Json::Value& data) -> void; + auto update_state_handler(const std::string& event, const Json::Value& data) -> void; + + public: + static auto get_instance() -> std::shared_ptr; + auto send(const std::string& method, Json::Value&& data) -> Json::Value; + auto register_handler(const std::string& event, const EventHandler& handler) -> void; + auto unregister_handler(EventHandler& handler) -> void; + + auto lock_state() -> std::lock_guard { return std::lock_guard{state_mutex}; } + auto& get_outputs() const { return state.outputs; } + auto& get_wsets() const { return state.wsets; } + auto& get_views() const { return state.views; } + auto& get_focused_output_name() const { return state.focused_output_name; } +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wayfire/window.hpp b/include/modules/wayfire/window.hpp new file mode 100644 index 00000000..3e8cb291 --- /dev/null +++ b/include/modules/wayfire/window.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +class Window : public AAppIconLabel { + std::shared_ptr ipc; + EventHandler handler; + + const Bar& bar_; + std::string old_app_id_; + + public: + Window(const std::string& id, const Bar& bar, const Json::Value& config); + ~Window() override; + + auto update() -> void override; + auto update_icon_label() -> void; +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wayfire/workspaces.hpp b/include/modules/wayfire/workspaces.hpp new file mode 100644 index 00000000..ab7cac44 --- /dev/null +++ b/include/modules/wayfire/workspaces.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +class Workspaces : public AModule { + std::shared_ptr ipc; + EventHandler handler; + + const Bar& bar_; + Gtk::Box box_; + std::vector buttons_; + + auto handleScroll(GdkEventScroll* e) -> bool override; + auto update() -> void override; + auto update_box() -> void; + + public: + Workspaces(const std::string& id, const Bar& bar, const Json::Value& config); + ~Workspaces() override; +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 6255b95f..e0033e8a 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -18,7 +18,7 @@ class Wireplumber : public ALabel { private: void asyncLoadRequiredApiModules(); - void prepare(); + void prepare(waybar::modules::Wireplumber* self); void activatePlugins(); static void updateVolume(waybar::modules::Wireplumber* self, uint32_t id); static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id); @@ -32,6 +32,8 @@ class Wireplumber : public ALabel { bool handleScroll(GdkEventScroll* e) override; + static std::list modules; + WpCore* wp_core_; GPtrArray* apis_; WpObjectManager* om_; @@ -44,6 +46,7 @@ class Wireplumber : public ALabel { double min_step_; uint32_t node_id_{0}; std::string node_name_; + gchar* type_; }; } // namespace waybar::modules diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 2f53103e..3737ae26 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -38,6 +38,8 @@ class AudioBackend { std::string desc_; std::string monitor_; std::string current_sink_name_; + std::string default_sink_name; + bool default_sink_running_; bool current_sink_running_; // SOURCE uint32_t source_idx_{0}; diff --git a/include/util/clara.hpp b/include/util/clara.hpp index da7151fe..73fa5415 100644 --- a/include/util/clara.hpp +++ b/include/util/clara.hpp @@ -622,8 +622,8 @@ inline auto convertInto(std::string const &source, bool &target) -> ParserResult } #ifdef CLARA_CONFIG_OPTIONAL_TYPE template -inline auto convertInto(std::string const &source, - CLARA_CONFIG_OPTIONAL_TYPE &target) -> ParserResult { +inline auto convertInto(std::string const &source, CLARA_CONFIG_OPTIONAL_TYPE &target) + -> ParserResult { T temp; auto result = convertInto(source, temp); if (result) target = std::move(temp); @@ -751,8 +751,8 @@ class ParserBase { public: virtual ~ParserBase() = default; virtual auto validate() const -> Result { return Result::ok(); } - virtual auto parse(std::string const &exeName, - TokenStream const &tokens) const -> InternalParseResult = 0; + virtual auto parse(std::string const &exeName, TokenStream const &tokens) const + -> InternalParseResult = 0; virtual auto cardinality() const -> size_t { return 1; } auto parse(Args const &args) const -> InternalParseResult { @@ -1098,8 +1098,8 @@ struct Parser : ParserBase { using ParserBase::parse; - auto parse(std::string const &exeName, - TokenStream const &tokens) const -> InternalParseResult override { + auto parse(std::string const &exeName, TokenStream const &tokens) const + -> InternalParseResult override { struct ParserInfo { ParserBase const *parser = nullptr; size_t count = 0; diff --git a/include/util/date.hpp b/include/util/date.hpp index a467cc56..d8653faf 100644 --- a/include/util/date.hpp +++ b/include/util/date.hpp @@ -15,7 +15,7 @@ namespace date { #if HAVE_CHRONO_TIMEZONES using namespace std::chrono; -using namespace std; +using std::format; #else using system_clock = std::chrono::system_clock; @@ -73,5 +73,3 @@ struct fmt::formatter> { } }; #endif - -using namespace date; diff --git a/include/util/gtk_icon.hpp b/include/util/gtk_icon.hpp index 44555f65..06f15abe 100644 --- a/include/util/gtk_icon.hpp +++ b/include/util/gtk_icon.hpp @@ -10,5 +10,7 @@ class DefaultGtkIconThemeWrapper { public: static bool has_icon(const std::string&); - static Glib::RefPtr load_icon(const char*, int, Gtk::IconLookupFlags); + static Glib::RefPtr load_icon( + const char*, int, Gtk::IconLookupFlags, + Glib::RefPtr style = Glib::RefPtr()); }; diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index 7b8df018..54da7d16 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -25,6 +25,7 @@ class PrivacyNodeInfo { std::string media_name; std::string node_name; std::string application_name; + bool is_monitor = false; std::string pipewire_access_portal_app_id; std::string application_icon_name; diff --git a/include/util/portal.hpp b/include/util/portal.hpp index 23619169..bff74b11 100644 --- a/include/util/portal.hpp +++ b/include/util/portal.hpp @@ -6,14 +6,12 @@ namespace waybar { -using namespace Gio; - enum class Appearance { UNKNOWN = 0, DARK = 1, LIGHT = 2, }; -class Portal : private DBus::Proxy { +class Portal : private Gio::DBus::Proxy { public: Portal(); void refreshAppearance(); diff --git a/include/util/prepare_for_sleep.h b/include/util/prepare_for_sleep.h index 68db8d8e..82f3b627 100644 --- a/include/util/prepare_for_sleep.h +++ b/include/util/prepare_for_sleep.h @@ -4,6 +4,6 @@ namespace waybar::util { -// Get a signal emited with value true when entering sleep, and false when exiting +// Get a signal emitted with value true when entering sleep, and false when exiting SafeSignal& prepare_for_sleep(); } // namespace waybar::util diff --git a/man/waybar-backlight-slider.5.scd b/man/waybar-backlight-slider.5.scd index cd5b5184..c6f027a1 100644 --- a/man/waybar-backlight-slider.5.scd +++ b/man/waybar-backlight-slider.5.scd @@ -31,11 +31,16 @@ The brightness can be controlled by dragging the slider across the bar or clicki typeof: string ++ The name of the preferred device to control. If left empty, a device will be chosen automatically. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` "modules-right": [ - "backlight-slider", + "backlight/slider", ], "backlight/slider": { "min": 0, diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index e1a688db..20810051 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -99,6 +99,11 @@ The *backlight* module displays the current backlight level. typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLE: ``` diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 29cb7d6d..a3c21a20 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -122,6 +122,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index bd64f457..fd7d5fb5 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -142,6 +142,11 @@ Addressed by *bluetooth* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: Status of the bluetooth device. diff --git a/man/waybar-cffi.5.scd b/man/waybar-cffi.5.scd index 926511d8..0203ef77 100644 --- a/man/waybar-cffi.5.scd +++ b/man/waybar-cffi.5.scd @@ -15,6 +15,11 @@ Addressed by *cffi/* typeof: string ++ The path to the dynamic library to load to control the widget. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + Some additional configuration may be required depending on the cffi dynamic library being used. diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 3947266d..50a5fc07 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -96,6 +96,10 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ array :[ :[ The actions corresponding to the buttons of the menu. +|[ *expand*: +:[ bool +:[ false +:[ Enables this module to consume all left over space dynamically. View all valid format options in *strftime(3)* or have a look https://en.cppreference.com/w/cpp/chrono/duration/formatter @@ -113,7 +117,7 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ 3 :[ Relevant for *mode=year*. Count of months per row |[ *weeks-pos* -:[ integer +:[ string :[ :[ The position where week numbers should be displayed. Disabled when is empty. Possible values: left|right diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 6b13a563..287bf123 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -82,6 +82,11 @@ The *cpu* module displays the current CPU utilization. default: true ++ Option to disable tooltip on hover. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{load}*: Current CPU load. diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 6b96d2a4..309fc184 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -138,6 +138,11 @@ Addressed by *custom/* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # RETURN-TYPE When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 1699a511..00af2b90 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -106,6 +106,11 @@ Addressed by *disk* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{percentage_used}*: Percentage of disk in use. diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index 07c94be9..a2146dfd 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -26,6 +26,11 @@ Addressed by *dwl/tags* default: false ++ If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLE ``` diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index f185c82c..9ac33d94 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -84,6 +84,11 @@ Addressed by *dwl/window* default: 24 ++ Option to change the size of the application icon. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{title}*: The title of the focused window. diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index 492e9850..a6ca9af0 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -61,6 +61,11 @@ Feral Gamemode optimizations. default: 4 ++ Defines the spacing between the icon and the text. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{glyph}*: The string icon glyph to use instead. diff --git a/man/waybar-gps.5.scd b/man/waybar-gps.5.scd new file mode 100644 index 00000000..e7473601 --- /dev/null +++ b/man/waybar-gps.5.scd @@ -0,0 +1,111 @@ +waybar-gps(5) "waybar-gps" "User Manual" + +# NAME + +waybar - gps module + +# DESCRIPTION + +*gps* module for gpsd. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libgps lives in: + +. /usr/lib/libgps.so or /usr/lib64/libgps.so +. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc +. /usr/include/gps + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {glyph} ++ + The text format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: Games running: {glyph} ++ + The text format of the tooltip. + +*interval*: ++ + typeof: integer ++ + default: 5 ++ + The interval in which the GPS information gets polled (e.g. current speed). + Significant updates (e.g. the current fix mode) are updated immediately. + +*hide-disconnected*: ++ + typeof: bool ++ + default: true ++ + Defines if the module should be hidden if there is no GPS receiver. + +*hide-no-fix*: ++ + typeof: bool ++ + default: false ++ + Defines if the module should be hidden if there is no GPS fix. + +# FORMAT REPLACEMENTS + +*{mode}*: Fix mode + +*{status}*: Technology used for GPS fix. Not all GPS receivers report this. + +*{latitude}*: Latitude, decimal degrees. Can be NaN. + +*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. + +*{longitude}*: Longitude, decimal degrees. Can be NaN. + +*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. + +*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. + +*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. + +*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. + +*{speed}*: Speed over ground, meters/sec. Can be NaN. + +*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. + +*{climb}*: Vertical speed, meters/sec. Can be NaN. + +*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. + +*{satellites_visible}*: Number of satellites visible from the GPS receiver. + +*{satellites_used}*: Number of satellites used for the GPS fix. + +# EXAMPLES + +``` +"gps": { + "format": "{mode}", + "format-disabled": "", // an empty format will hide the module + "format-no-fix": "No fix", + "format-fix-3d": "{status}", + "tooltip-format": "{mode}", + "tooltip-format-no-fix": "{satellites_visible} satellites visible", + "tooltip-format-fix-2d": "{satellites_used}/{satellites_visible} satellites used", + "tooltip-format-fix-3d": "Altitude: {altitude_hae}m", + "hide-disconnected": false +} +``` +# STYLE + +- *#gps* +- *#gps.disabled* Applied when GPS is disabled. +- *#gps.fix-none* Applied when GPS is present, but there is no fix. +- *#gps.fix-2d* Applied when there is a 2D fix. +- *#gps.fix-3d* Applied when there is a 3D fix. diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 33b28ae4..5a7ba941 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -38,6 +38,11 @@ Addressed by *hyprland/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS @@ -54,9 +59,9 @@ Addressed by *hyprland/language* ``` "hyprland/language": { - "format": "Lang: {long}" - "format-en": "AMERICA, HELL YEAH!" - "format-tr": "As bayrakları" + "format": "Lang: {long}", + "format-en": "AMERICA, HELL YEAH!", + "format-tr": "As bayrakları", "keyboard-name": "at-translated-set-2-keyboard" } ``` diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd index e27138e7..f6cdff94 100644 --- a/man/waybar-hyprland-submap.5.scd +++ b/man/waybar-hyprland-submap.5.scd @@ -93,6 +93,11 @@ Addressed by *hyprland/submap* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd index 4e9c5d18..34ebf89b 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -35,6 +35,11 @@ Addressed by *hyprland/window* default: 24 ++ Option to change the size of the application icon. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS See the output of "hyprctl clients" for examples diff --git a/man/waybar-hyprland-windowcount.5.scd b/man/waybar-hyprland-windowcount.5.scd new file mode 100644 index 00000000..6a4f87d1 --- /dev/null +++ b/man/waybar-hyprland-windowcount.5.scd @@ -0,0 +1,46 @@ +waybar-hyprland-windowcount(5) + +# NAME + +waybar - hyprland window count module + +# DESCRIPTION + +The *windowcount* module displays the number of windows in the current Hyprland workspace. + +# CONFIGURATION + +Addressed by *hyprland/windowcount* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format for how information should be displayed. On {} the current workspace window count is displayed. + +*format-empty*: ++ + typeof: string ++ + Override the format when the workspace contains no windows window + +*format-windowed*: ++ + typeof: string ++ + Override the format when the workspace contains no fullscreen windows + +*format-fullscreen*: ++ + typeof: string ++ + Override the format when the workspace contains a fullscreen window + +*separate-outputs*: ++ + typeof: bool ++ + default: true ++ + Show the active workspace window count of the monitor the bar belongs to, instead of the focused workspace. + +# STYLE + +- *#windowcount* + +The following classes are applied to the entire Waybar rather than just the +windowcount widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.fullscreen* When there is a fullscreen window in the workspace; + useful with Hyprland's *fullscreen, 1* mode diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index c71168d4..f48d6f78 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -48,6 +48,11 @@ Addressed by *hyprland/workspaces* default: false ++ If this and show-special are to true, special workspaces will be shown only if visible. +*persistent-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only persistent workspaces will be shown on bar. + *all-outputs*: ++ typeof: bool ++ default: false ++ @@ -76,8 +81,14 @@ Addressed by *hyprland/workspaces* If set to number, workspaces will sort by number. If set to name, workspaces will sort by name. If set to id, workspaces will sort by id. + If set to special-centered, workspaces will sort by default with special workspaces in the center. If none of those, workspaces will sort with default behavior. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{id}*: id of workspace assigned by compositor diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 81a097a7..405c8fc5 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -102,6 +102,11 @@ screensaver, also known as "presentation mode". typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) @@ -120,3 +125,9 @@ screensaver, also known as "presentation mode". "timeout": 30.5 } ``` + +# STYLE + +- *#idle_inhibitor* +- *#idle_inhibitor.activated* +- *#idle_inhibitor.deactivated* diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index e3a69e38..a2dcc938 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -64,6 +64,11 @@ The *image* module displays an image from a path. default: true ++ Option to enable tooltip on hover. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # SCRIPT OUTPUT Similar to the *custom* module, output values of the script are *newline* separated. diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd index 5513cc49..fce6f4f8 100644 --- a/man/waybar-inhibitor.5.scd +++ b/man/waybar-inhibitor.5.scd @@ -89,6 +89,11 @@ See *systemd-inhibit*(1) for more information. typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index 180143b7..85ce7180 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -98,6 +98,11 @@ Addressed by *jack* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{load}*: The current CPU load estimated by JACK. diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index 9ecc5515..79498414 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -53,6 +53,11 @@ You must be a member of the input group to use this module. default: [58, 69, 70] ++ Customize the key to trigger this module, the key number can be found in /usr/include/linux/input-event-codes.h or running sudo libinput debug-events --show-keycodes. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{name}*: Caps, Num, or Scroll. diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 5c368ae8..567c2c72 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -97,6 +97,11 @@ Addressed by *memory* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{percentage}*: Percentage of memory in use. @@ -115,6 +120,8 @@ Addressed by *memory* *{swapAvail}*: Amount of available swap in GiB. +*{swapState}*: Signals if swap is activated or not + # EXAMPLES ``` diff --git a/man/waybar-menu.5.scd b/man/waybar-menu.5.scd index 19790ed4..47e10432 100644 --- a/man/waybar-menu.5.scd +++ b/man/waybar-menu.5.scd @@ -7,7 +7,7 @@ waybar - menu property # OVERVIEW -Some modules support a 'menu', which allows to have a popup menu whan a defined +Some modules support a 'menu', which allows to have a popup menu when a defined click is done over the module. # PROPERTIES diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index c576a5c0..84abc2e8 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -175,6 +175,11 @@ Addressed by *mpd* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS ## WHEN PLAYING/PAUSED diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd index 380a1a19..9c192bd1 100644 --- a/man/waybar-mpris.5.scd +++ b/man/waybar-mpris.5.scd @@ -148,6 +148,11 @@ The *mpris* module displays currently playing media via libplayerctl. typeof: map[string]string ++ Allows setting _{status_icon}_ based on player status (playing, paused, stopped). +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index e1cf810d..3b63e3ee 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -24,7 +24,7 @@ Addressed by *network* *family*: ++ typeof: string ++ default: *ipv4* ++ - The address family that is used for the format replacement {ipaddr} and to determine if a network connection is present. + The address family that is used for the format replacement {ipaddr} and to determine if a network connection is present. Set it to ipv4_6 to display both. *format*: ++ typeof: string ++ @@ -142,6 +142,11 @@ Addressed by *network* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{ifname}*: Name of the network interface. @@ -150,9 +155,13 @@ Addressed by *network* *{gwaddr}*: The default gateway for the interface -*{netmask}*: The subnetmask corresponding to the IP. +*{netmask}*: The subnetmask corresponding to the IP(V4). -*{cidr}*: The subnetmask corresponding to the IP in CIDR notation. +*{netmask6}*: The subnetmask corresponding to the IP(V6). + +*{cidr}*: The subnetmask corresponding to the IP(V4) in CIDR notation. + +*{cidr6}*: The subnetmask corresponding to the IP(V6) in CIDR notation. *{essid}*: Name (SSID) of the wireless network. @@ -162,7 +171,7 @@ Addressed by *network* *{signaldBm}*: Signal strength of the wireless network in dBm. -*{frequency}*: Frequency of the wireless network in MHz. +*{frequency}*: Frequency of the wireless network in GHz. *{bandwidthUpBits}*: Instant up speed in bits/seconds. diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd index 6895d25c..44876fd9 100644 --- a/man/waybar-niri-language.5.scd +++ b/man/waybar-niri-language.5.scd @@ -33,6 +33,11 @@ Addressed by *niri/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{short}*: Short name of layout (e.g. "us"). Equals to {}. diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd index 9e2e9f63..8e886c29 100644 --- a/man/waybar-niri-window.5.scd +++ b/man/waybar-niri-window.5.scd @@ -35,6 +35,11 @@ Addressed by *niri/window* default: 24 ++ Option to change the size of the application icon. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS See the output of "niri msg windows" for examples diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 50e497cd..4be85eb3 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -45,6 +45,11 @@ Addressed by *niri/workspaces* typeof: string ++ Command to execute when the module is updated. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, or index for unnamed workspaces, @@ -65,6 +70,8 @@ Additional to workspace name matching, the following *format-icons* can be set. - *default*: Will be shown, when no string matches are found. - *focused*: Will be shown, when workspace is focused. - *active*: Will be shown, when workspace is active on its output. +- *urgent*: Will be shown, when workspace has urgent windows. +- *empty*: Will be shown, when workspace is empty. # EXAMPLES @@ -90,6 +97,7 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button* - *#workspaces button.focused*: The single focused workspace. - *#workspaces button.active*: The workspace is active (visible) on its output. +- *#workspaces button.urgent*: The workspace has one or more urgent windows. - *#workspaces button.empty*: The workspace is empty. - *#workspaces button.current_output*: The workspace is from the same output as the bar that it is displayed on. diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index 82fad13b..6488767b 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -35,6 +35,13 @@ $XDG_CONFIG_HOME/waybar/config :[ object :[ See default value in the example below. :[ Icons used to represent the various power-profile. *Note*: the default configuration uses the font-awesome icons. You may want to override it if you don't have this font installed on your system. +|[ *expand*: +:[ bool +:[ false +:[ Enables this module to consume all left over space dynamically. + + + # CONFIGURATION EXAMPLES diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd index d13d8ed3..1c4ef59d 100644 --- a/man/waybar-privacy.5.scd +++ b/man/waybar-privacy.5.scd @@ -32,6 +32,22 @@ the screen or playing audio. Which privacy modules to monitor. See *MODULES CONFIGURATION* for++ more information. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*ignore-monitor* ++ + typeof: bool ++ + default: true ++ + Ignore streams with *stream.monitor* property. + +*ignore* ++ + typeof: array of objects ++ + default: [] ++ + Additional streams to be ignored. See *IGNORE CONFIGURATION* for++ + more information. + # MODULES CONFIGURATION *type*: ++ @@ -49,6 +65,14 @@ the screen or playing audio. default: 24 ++ The size of each icon in the tooltip. +# IGNORE CONFIGURATION + +*type*: ++ + typeof: string + +*name*: ++ + typeof: string + # EXAMPLES ``` @@ -72,6 +96,17 @@ the screen or playing audio. "tooltip": true, "tooltip-icon-size": 24 } + ], + "ignore-monitor": true, + "ignore": [ + { + "type": "audio-in", + "name": "cava" + }, + { + "type": "screenshare", + "name": "obs" + } ] }, ``` diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index cf07fed1..cb274826 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -27,6 +27,11 @@ The volume can be controlled by dragging the slider across the bar or clicking o default: horizontal ++ The orientation of the slider. Can be either `horizontal` or `vertical`. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index 5b38e8b7..d47fc744 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -126,6 +126,11 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{desc}*: Pulseaudio port's description, for bluetooth it'll be the device name. diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index 1368bda9..78e03634 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -64,6 +64,11 @@ Addressed by *river/layout* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLE ``` diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index b992fdaf..5837411d 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -78,6 +78,11 @@ Addressed by *river/mode* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index f0b2b84e..64621229 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -26,6 +26,16 @@ Addressed by *river/tags* default: false ++ If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*hide-vacant*: ++ + typeof: bool ++ + default: false ++ + Only show relevant tags: tags that are either focused or have a window on them. + # EXAMPLE ``` diff --git a/man/waybar-river-window.5.scd b/man/waybar-river-window.5.scd index 6db9a2fa..82eee0a5 100644 --- a/man/waybar-river-window.5.scd +++ b/man/waybar-river-window.5.scd @@ -62,6 +62,11 @@ Addressed by *river/window* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd index 03dfe0af..f14d35e9 100644 --- a/man/waybar-sndio.5.scd +++ b/man/waybar-sndio.5.scd @@ -87,6 +87,11 @@ cursor is over the module, and clicking on the module toggles mute. typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{volume}*: Volume in percentage. diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index a1fc5d08..5710e69d 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -45,6 +45,11 @@ Addressed by *sway/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{short}*: Short name of layout (e.g. "us"). Equals to {}. diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 8d5d7c2c..52827376 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -83,6 +83,11 @@ Addressed by *sway/mode* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` diff --git a/man/waybar-sway-scratchpad.5.scd b/man/waybar-sway-scratchpad.5.scd index 5ae104bc..e51ad12a 100644 --- a/man/waybar-sway-scratchpad.5.scd +++ b/man/waybar-sway-scratchpad.5.scd @@ -49,6 +49,11 @@ Addressed by *sway/scratchpad* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{icon}*: Icon, as defined in *format-icons*. diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 6d1e3196..6fa9170b 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -89,6 +89,11 @@ Addressed by *sway/window* default: false ++ If the workspace itself is focused and the workspace contains nodes or floating_nodes, show the workspace name. If not set, text remains empty but styles according to nodes in the workspace are still applied. +*show-hidden-marks*: ++ + typeof: bool ++ + default: false ++ + For the *{marks}* format replacement, include hidden marks that start with an underscore. + *rewrite*: ++ typeof: object ++ Rules to rewrite the module format output. See *rewrite rules*. @@ -103,6 +108,11 @@ Addressed by *sway/window* default: 24 ++ Option to change the size of the application icon. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{title}*: The title of the focused window. @@ -112,6 +122,8 @@ Addressed by *sway/window* *{shell}*: The shell of the focused window. It's 'xwayland' when the window is running through xwayland, otherwise, it's 'xdg-shell'. +*{marks}*: Marks of the window. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index fc73a85a..1c4360e9 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -88,6 +88,7 @@ warp-on-scroll: ++ Keys are the rules, while the values are the methods of representation. Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching. You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. + For Wayland windows `class` is matched against the `app_id`, and for X11 windows against the `class` property. *window-rewrite-default*: typeof: string ++ @@ -99,6 +100,11 @@ warp-on-scroll: ++ default: " " ++ The separator to be used between windows in a workspace. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index ada3ab8b..8d7c980a 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -49,6 +49,11 @@ Addressed by *systemd-failed-units* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{nr_failed_system}*: Number of failed units from systemwide (PID=1) systemd. @@ -57,6 +62,12 @@ Addressed by *systemd-failed-units* *{nr_failed}*: Number of total failed units. +*{systemd_state}:* State of the systemd system session + +*{user_state}:* State of the systemd user session + +*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") + # EXAMPLES ``` diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 4ca17410..507b9190 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -36,6 +36,10 @@ Addressed by *temperature* The substring to search for in */sys/class/hwmon/hwmonX/name* (where hwmonX is any folder in */sys/class/hwmon/*). Waybar will search for every directory in */sys/class/hwmon/* and uses the directory in which the *name* matches *hwmon-by-name*. +*warning-threshold*: ++ + typeof: integer ++ + The threshold before it is considered warning (Celsius). + *critical-threshold*: ++ typeof: integer ++ The threshold before it is considered critical (Celsius). @@ -45,6 +49,10 @@ Addressed by *temperature* default: 10 ++ The interval in which the information gets polled. +*format-warning*: ++ + typeof: string ++ + The format to use when temperature is considered warning + *format-critical*: ++ typeof: string ++ The format to use when temperature is considered critical @@ -129,6 +137,11 @@ Addressed by *temperature* typeof: array ++ The actions corresponding to the buttons of the menu. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{temperatureC}*: Temperature in Celsius. diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index 294a261d..dec5347f 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -37,12 +37,21 @@ Addressed by *tray* typeof: string ++ Command to execute when the module is updated. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` "tray": { "icon-size": 21, - "spacing": 10 + "spacing": 10, + "icons": { + "blueman": "bluetooth", + "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" + } } ``` diff --git a/man/waybar-wayfire-window.5.scd b/man/waybar-wayfire-window.5.scd new file mode 100644 index 00000000..290b0c65 --- /dev/null +++ b/man/waybar-wayfire-window.5.scd @@ -0,0 +1,82 @@ +waybar-wayfire-window(5) + +# NAME + +waybar - wayfire window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in wayfire. + +# CONFIGURATION + +Addressed by *wayfire/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. On {} the current window title is displayed. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite window title. See *rewrite rules*. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +See the output of "wayfire msg windows" for examples + +*{title}*: The current title of the focused window. + +*{app_id}*: The current app ID of the focused window. + +# REWRITE RULES + +*rewrite* is an object where keys are regular expressions and values are +rewrite rules if the expression matches. Rules may contain references to +captures of the expression. + +Regular expression and replacement follow ECMA-script rules. + +If no expression matches, the title is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"wayfire/window": { + "format": "{}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` + +# STYLE + +- *#window* +- *window#waybar.empty #window* When no windows are on the workspace + +The following classes are applied to the entire Waybar rather than just the +window widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.solo* When only one window is on the workspace +- *window#waybar.* Where *app-id* is the app ID of the only window on + the workspace diff --git a/man/waybar-wayfire-workspaces.5.scd b/man/waybar-wayfire-workspaces.5.scd new file mode 100644 index 00000000..53a179e8 --- /dev/null +++ b/man/waybar-wayfire-workspaces.5.scd @@ -0,0 +1,86 @@ +waybar-wayfire-workspaces(5) + +# NAME + +waybar - wayfire workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in wayfire. + +# CONFIGURATION + +Addressed by *wayfire/workspaces* + +*format*: ++ + typeof: string ++ + default: {value} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace name, index and state, the corresponding icon gets selected. See *icons*. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can click to change workspace. If set to true this behaviour is disabled. + +*disable-markup*: ++ + typeof: bool ++ + default: false ++ + If set to true, button label will escape pango markup. + +*current-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the active or focused workspace will be shown. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{icon}*: Icon, as defined in *format-icons*. + +*{index}*: Index of the workspace on its output. + +*{output}*: Output where the workspace is located. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string matches are found. +- *focused*: Will be shown, when workspace is focused. + +# EXAMPLES + +``` +"wayfire/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "focused": "", + "default": "" + } +} +``` + +# Style + +- *#workspaces button* +- *#workspaces button.focused*: The single focused workspace. +- *#workspaces button.empty*: The workspace is empty. +- *#workspaces button.current_output*: The workspace is from the same output as + the bar that it is displayed on. diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index 9c26ebaf..ae78f184 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -19,6 +19,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber. typeof: string ++ This format is used when the sound is muted. +*node-type*: ++ + typeof: string ++ + default: *Audio/Sink* ++ + The WirePlumber node type to attach to. Use *Audio/Source* to manage microphones etc. + *tooltip*: ++ typeof: bool ++ default: *true* ++ @@ -108,6 +113,8 @@ The *wireplumber* module displays the current volume reported by WirePlumber. # EXAMPLES +## Basic: + ``` "wireplumber": { "format": "{volume}%", @@ -116,6 +123,26 @@ The *wireplumber* module displays the current volume reported by WirePlumber. } ``` +## Separate Sink and Source Widgets + +``` +"wireplumber#sink": { + "format": "{volume}% {icon}", + "format-muted": "", + "format-icons": ["", "", ""], + "on-click": "helvum", + "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", + "scroll-step": 5 +}, +"wireplumber#source": { + "node-type": "Audio/Source", + "format": "{volume}% ", + "format-muted": "", + "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", + "scroll-step": 5 +} +``` + # STYLE - *#wireplumber* diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index f3a89656..6ca0aa99 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -23,6 +23,21 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # BAR CONFIGURATION +*expand-center* ++ + typeof: bool ++ + default: false ++ + Enables the modules-center to consume all left over space dynamically. + +*expand-left* ++ + typeof: bool ++ + default: false ++ + Enables the modules-left to consume all left over space dynamically. + +*expand-right* ++ + typeof: bool ++ + default: false ++ + Enables the modules-left to consume all left over space dynamically. + *layer* ++ typeof: string ++ default: bottom ++ @@ -68,6 +83,11 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( typeof: integer ++ Margins value without units. +*no-center* ++ + typeof: bool ++ + default: false ++ + Option to disable the center modules fully useful together with expand-\*. + *spacing* ++ typeof: integer ++ Size of gaps in between the different modules. @@ -181,6 +201,19 @@ A minimal *config* file could look like this: } ``` +# SIGNALS + +Waybar accepts the following signals: + +*SIGUSR1* + Toggles the bar visibility (hides if shown, shows if hidden) +*SIGUSR2* + Reloads (resets) the bar +*SIGINT* + Quits the bar + +For example, to toggle the bar programmatically, you can invoke `killall -SIGUSR1 waybar`. + # MULTI OUTPUT CONFIGURATION ## Limit a configuration to some outputs @@ -239,6 +272,17 @@ When positioning Waybar on the left or right side of the screen, sometimes it's Valid options for the "rotate" property are: 0, 90, 180, and 270. +## Swapping icon and label + +If a module displays both a label and an icon, it might be desirable to swap them (for instance, for panels on the left or right of the screen, or for user adopting a right-to-left script). This can be achieved with the "swap-icon-label" property, taking a boolean. Example: +``` +{ + "sway/window": { + "swap-icon-label": true + } +} +``` + ## Grouping modules Module groups allow stacking modules in any direction. By default, when the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally. This can be changed with the "orientation" property. diff --git a/meson.build b/meson.build index 726d492b..8832a55e 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'waybar', 'cpp', 'c', - version: '0.11.0', + version: '0.13.0', license: 'MIT', meson_version: '>= 0.59.0', default_options : [ @@ -93,6 +93,7 @@ libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) +libgps = dependency('libgps', required: get_option('gps')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -306,6 +307,7 @@ if true 'src/modules/hyprland/language.cpp', 'src/modules/hyprland/submap.cpp', 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/windowcount.cpp', 'src/modules/hyprland/workspace.cpp', 'src/modules/hyprland/workspaces.cpp', 'src/modules/hyprland/windowcreationpayload.cpp', @@ -333,6 +335,19 @@ if get_option('niri') ) endif +if true + add_project_arguments('-DHAVE_WAYFIRE', language: 'cpp') + src_files += files( + 'src/modules/wayfire/backend.cpp', + 'src/modules/wayfire/window.cpp', + 'src/modules/wayfire/workspaces.cpp', + ) +endif + +if get_option('login-proxy') + add_project_arguments('-DHAVE_LOGIN_PROXY', language: 'cpp') +endif + if libnl.found() and libnlgen.found() add_project_arguments('-DHAVE_LIBNL', language: 'cpp') src_files += files('src/modules/network.cpp') @@ -482,7 +497,7 @@ if get_option('experimental') endif cava = dependency('cava', - version : '>=0.10.3', + version : '>=0.10.4', required: get_option('cava'), fallback : ['cava', 'cava_dep'], not_found_message: 'cava is not found. Building waybar without cava') @@ -493,6 +508,12 @@ if cava.found() man_files += files('man/waybar-cava.5.scd') endif +if libgps.found() + add_project_arguments('-DHAVE_LIBGPS', language: 'cpp') + src_files += files('src/modules/gps.cpp') + man_files += files('man/waybar-gps.5.scd') +endif + subdir('protocol') app_resources = [] @@ -531,7 +552,8 @@ executable( libsndio, tz_dep, xkbregistry, - cava + cava, + libgps ], include_directories: inc_dirs, install: true, diff --git a/meson_options.txt b/meson_options.txt index 303ef038..c0f63f28 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -20,3 +20,5 @@ option('jack', type: 'feature', value: 'auto', description: 'Enable support for option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') option('niri', type: 'boolean', description: 'Enable support for niri') +option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') +option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') diff --git a/nix/default.nix b/nix/default.nix index a9ff180b..2c97c20d 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,42 +1,47 @@ -{ lib -, pkgs -, waybar -, version +{ + lib, + pkgs, + waybar, + version, }: let libcava = rec { - version = "0.10.3"; + version = "0.10.4"; src = pkgs.fetchFromGitHub { owner = "LukashonakV"; repo = "cava"; - rev = version; - hash = "sha256-ZDFbI69ECsUTjbhlw2kHRufZbQMu+FQSMmncCJ5pagg="; + tag = version; + hash = "sha256-9eTDqM+O1tA/3bEfd1apm8LbEcR9CVgELTIspSVPMKM="; }; }; in -(waybar.overrideAttrs ( - oldAttrs: { - inherit version; +(waybar.overrideAttrs (oldAttrs: { + inherit version; - src = lib.cleanSourceWith { - filter = name: type: type != "regular" || !lib.hasSuffix ".nix" name; - src = lib.cleanSource ../.; - }; + src = lib.cleanSourceWith { + filter = name: type: type != "regular" || !lib.hasSuffix ".nix" name; + src = lib.cleanSource ../.; + }; - mesonFlags = lib.remove "-Dgtk-layer-shell=enabled" oldAttrs.mesonFlags; + mesonFlags = lib.remove "-Dgtk-layer-shell=enabled" oldAttrs.mesonFlags; - # downstream patch should not affect upstream - patches = []; + # downstream patch should not affect upstream + patches = [ ]; + # nixpkgs checks version, no need when building locally + nativeInstallCheckInputs = [ ]; - buildInputs = (builtins.filter (p: p.pname != "wireplumber") oldAttrs.buildInputs) ++ [ - pkgs.wireplumber - ]; + buildInputs = (builtins.filter (p: + p.pname != "wireplumber" && + p.pname != "gps" + ) oldAttrs.buildInputs) ++ [ + pkgs.wireplumber + pkgs.gpsd + ]; - postUnpack = '' - pushd "$sourceRoot" - cp -R --no-preserve=mode,ownership ${libcava.src} subprojects/cava-${libcava.version} - patchShebangs . - popd - ''; - } -)) + postUnpack = '' + pushd "$sourceRoot" + cp -R --no-preserve=mode,ownership ${libcava.src} subprojects/cava-${libcava.version} + patchShebangs . + popd + ''; +})) diff --git a/resources/config.jsonc b/resources/config.jsonc index 6ac1aa50..67d5ff5b 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -104,7 +104,11 @@ }, "tray": { // "icon-size": 21, - "spacing": 10 + "spacing": 10, + // "icons": { + // "blueman": "bluetooth", + // "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" + // } }, "clock": { // "timezone": "America/New_York", diff --git a/resources/custom_modules/cffi_example/main.c b/resources/custom_modules/cffi_example/main.c index ba2c8cf4..7618de58 100644 --- a/resources/custom_modules/cffi_example/main.c +++ b/resources/custom_modules/cffi_example/main.c @@ -17,7 +17,7 @@ void onclicked(GtkButton* button) { } // You must -const size_t wbcffi_version = 1; +const size_t wbcffi_version = 2; void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, size_t config_entries_len) { @@ -67,4 +67,4 @@ void wbcffi_refresh(void* instance, int signal) { void wbcffi_doaction(void* instance, const char* name) { printf("cffi_example inst=%p: doAction(%s)\n", instance, name); -} \ No newline at end of file +} diff --git a/resources/custom_modules/cffi_example/waybar_cffi_module.h b/resources/custom_modules/cffi_example/waybar_cffi_module.h index a7886bea..c1a82f59 100644 --- a/resources/custom_modules/cffi_example/waybar_cffi_module.h +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -7,7 +7,7 @@ extern "C" { #endif -/// Waybar ABI version. 1 is the latest version +/// Waybar ABI version. 2 is the latest version extern const size_t wbcffi_version; /// Private Waybar CFFI module @@ -35,7 +35,13 @@ typedef struct { typedef struct { /// Entry key const char* key; - /// Entry value as string. JSON object and arrays are serialized. + /// Entry value + /// + /// In ABI version 1, this may be either a bare string if the value is a + /// string, or the JSON representation of any other JSON object as a string. + /// + /// From ABI version 2 onwards, this is always the JSON representation of the + /// value as a string. const char* value; } wbcffi_config_entry; diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index d1bb72b4..524d4d2a 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -112,6 +112,7 @@ class PlayerManager: logger.debug(f"Metadata changed for player {player.props.player_name}") player_name = player.props.player_name artist = player.get_artist() + artist = artist.replace("&", "&") title = player.get_title() title = title.replace("&", "&") diff --git a/resources/waybar.service.in b/resources/waybar.service.in index 81ac6779..18bac54c 100644 --- a/resources/waybar.service.in +++ b/resources/waybar.service.in @@ -1,5 +1,5 @@ [Unit] -Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. +Description=Highly customizable Wayland bar for Sway and Wlroots based compositors Documentation=https://github.com/Alexays/Waybar/wiki/ PartOf=graphical-session.target After=graphical-session.target diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index d7ee666e..79cd5fe1 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -1,6 +1,7 @@ #include "AIconLabel.hpp" #include +#include namespace waybar { @@ -17,14 +18,36 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const box_.get_style_context()->add_class(id); } - box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); + int rot = 0; + + if (config_["rotate"].isUInt()) { + rot = config["rotate"].asUInt() % 360; + if ((rot % 90) != 00) rot = 0; + rot /= 90; + } + + if ((rot % 2) == 0) + box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); + else + box_.set_orientation(Gtk::Orientation::ORIENTATION_VERTICAL); box_.set_name(name); int spacing = config_["icon-spacing"].isInt() ? config_["icon-spacing"].asInt() : 8; box_.set_spacing(spacing); - box_.add(image_); - box_.add(label_); + bool swap_icon_label = false; + if (not config_["swap-icon-label"].isBool()) + spdlog::warn("'swap-icon-label' must be a bool."); + else + swap_icon_label = config_["swap-icon-label"].asBool(); + + if ((rot == 0 || rot == 3) ^ swap_icon_label) { + box_.add(image_); + box_.add(label_); + } else { + box_.add(label_); + box_.add(image_); + } event_box_.add(box_); } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 467572f1..c218e402 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -68,11 +68,11 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st // there might be "~" or "$HOME" in original path, try to expand it. auto result = Config::tryExpandPath(menuFile, ""); - if (!result.has_value()) { + if (result.empty()) { throw std::runtime_error("Failed to expand file: " + menuFile); } - menuFile = result.value(); + menuFile = result.front(); // Read the menu descriptor file std::ifstream file(menuFile); if (!file.is_open()) { diff --git a/src/AModule.cpp b/src/AModule.cpp index c180b480..259c6a39 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -15,6 +15,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: : name_(name), config_(config), isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, + isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), distance_scrolled_x_(0.0) { // Configure module action Map @@ -82,7 +83,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { - for (const auto& pid : pid_) { + for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); } @@ -92,15 +93,15 @@ AModule::~AModule() { auto AModule::update() -> void { // Run user-provided update handler if configured if (config_["on-update"].isString()) { - pid_.push_back(util::command::forkExec(config_["on-update"].asString())); + pid_children_.push_back(util::command::forkExec(config_["on-update"].asString())); } } // Get mapping between event name and module action name -// Then call overrided doAction in order to call appropriate module action +// Then call overridden doAction in order to call appropriate module action auto AModule::doAction(const std::string& name) -> void { if (!name.empty()) { const std::map::const_iterator& recA{eventActionMap_.find(name)}; - // Call overrided action if derrived class has implemented it + // Call overridden action if derived class has implemented it if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second); } } @@ -181,7 +182,7 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { format.clear(); } if (!format.empty()) { - pid_.push_back(util::command::forkExec(format)); + pid_children_.push_back(util::command::forkExec(format)); } dp.emit(); return true; @@ -266,13 +267,14 @@ bool AModule::handleScroll(GdkEventScroll* e) { this->AModule::doAction(eventName); // Second call user scripts if (config_[eventName].isString()) - pid_.push_back(util::command::forkExec(config_[eventName].asString())); + pid_children_.push_back(util::command::forkExec(config_[eventName].asString())); dp.emit(); return true; } bool AModule::tooltipEnabled() const { return isTooltip; } +bool AModule::expandEnabled() const { return isExpand; } AModule::operator Gtk::Widget&() { return event_box_; } diff --git a/src/bar.cpp b/src/bar.cpp index 5068e90d..3c3ab690 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -534,13 +534,21 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto waybar::Bar::setupWidgets() -> void { window.add(box_); - box_.pack_start(left_, false, false); - if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { - box_.set_center_widget(center_); - } else { - box_.pack_start(center_, true, false); + + bool expand_left = config["expand-left"].isBool() ? config["expand-left"].asBool() : false; + bool expand_center = config["expand-center"].isBool() ? config["expand-center"].asBool() : false; + bool expand_right = config["expand-right"].isBool() ? config["expand-right"].asBool() : false; + bool no_center = config["no-center"].isBool() ? config["no-center"].asBool() : false; + + box_.pack_start(left_, expand_left, expand_left); + if (!no_center) { + if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { + box_.set_center_widget(center_); + } else { + box_.pack_start(center_, true, expand_center); + } } - box_.pack_end(right_, false, false); + box_.pack_end(right_, expand_right, expand_right); // Convert to button code for every module that is used. setupAltFormatKeyForModuleList("modules-left"); @@ -549,17 +557,24 @@ auto waybar::Bar::setupWidgets() -> void { Factory factory(*this, config); getModules(factory, "modules-left"); - getModules(factory, "modules-center"); + if (!no_center) { + getModules(factory, "modules-center"); + } getModules(factory, "modules-right"); + for (auto const& module : modules_left_) { - left_.pack_start(*module, false, false); + left_.pack_start(*module, module->expandEnabled(), module->expandEnabled()); } - for (auto const& module : modules_center_) { - center_.pack_start(*module, false, false); + + if (!no_center) { + for (auto const& module : modules_center_) { + center_.pack_start(*module, module->expandEnabled(), module->expandEnabled()); + } } + std::reverse(modules_right_.begin(), modules_right_.end()); for (auto const& module : modules_right_) { - right_.pack_end(*module, false, false); + right_.pack_end(*module, module->expandEnabled(), module->expandEnabled()); } } diff --git a/src/client.cpp b/src/client.cpp index 63a9276a..946780db 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -86,7 +86,7 @@ void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_ } } } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::done: {}", e.what()); } } @@ -97,7 +97,7 @@ void waybar::Client::handleOutputName(void *data, struct zxdg_output_v1 * /*xdg_ auto &output = client->getOutput(data); output.name = name; } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::name: {}", e.what()); } } @@ -106,13 +106,13 @@ void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); - const char *open_paren = strrchr(description, '('); // Description format: "identifier (name)" - size_t identifier_length = open_paren - description; - output.identifier = std::string(description, identifier_length - 1); + auto s = std::string(description); + auto pos = s.find(" ("); + output.identifier = pos != std::string::npos ? s.substr(0, pos) : s; } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::description: {}", e.what()); } } @@ -151,15 +151,19 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon const std::string waybar::Client::getStyle(const std::string &style, std::optional appearance = std::nullopt) { + auto gtk_settings = Gtk::Settings::get_default(); std::optional css_file; + if (style.empty()) { std::vector search_files; switch (appearance.value_or(portal->getAppearance())) { case waybar::Appearance::LIGHT: search_files.emplace_back("style-light.css"); + gtk_settings->property_gtk_application_prefer_dark_theme() = false; break; case waybar::Appearance::DARK: search_files.emplace_back("style-dark.css"); + gtk_settings->property_gtk_application_prefer_dark_theme() = true; break; case waybar::Appearance::UNKNOWN: break; @@ -169,9 +173,11 @@ const std::string waybar::Client::getStyle(const std::string &style, } else { css_file = style; } + if (!css_file) { throw std::runtime_error("Missing required resource files"); } + spdlog::info("Using CSS file {}", css_file.value()); return css_file.value(); }; diff --git a/src/config.cpp b/src/config.cpp index 375dc4cb..7096ba89 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -21,8 +21,8 @@ const std::vector Config::CONFIG_DIRS = { const char *Config::CONFIG_PATH_ENV = "WAYBAR_CONFIG_DIR"; -std::optional Config::tryExpandPath(const std::string &base, - const std::string &filename) { +std::vector Config::tryExpandPath(const std::string &base, + const std::string &filename) { fs::path path; if (!filename.empty()) { @@ -33,33 +33,35 @@ std::optional Config::tryExpandPath(const std::string &base, spdlog::debug("Try expanding: {}", path.string()); + std::vector results; wordexp_t p; if (wordexp(path.c_str(), &p, 0) == 0) { - if (access(*p.we_wordv, F_OK) == 0) { - std::string result = *p.we_wordv; - wordfree(&p); - spdlog::debug("Found config file: {}", path.string()); - return result; + for (size_t i = 0; i < p.we_wordc; i++) { + if (access(p.we_wordv[i], F_OK) == 0) { + results.emplace_back(p.we_wordv[i]); + spdlog::debug("Found config file: {}", p.we_wordv[i]); + } } wordfree(&p); } - return std::nullopt; + + return results; } std::optional Config::findConfigPath(const std::vector &names, const std::vector &dirs) { if (const char *dir = std::getenv(Config::CONFIG_PATH_ENV)) { for (const auto &name : names) { - if (auto res = tryExpandPath(dir, name); res) { - return res; + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res.front(); } } } for (const auto &dir : dirs) { for (const auto &name : names) { - if (auto res = tryExpandPath(dir, name); res) { - return res; + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res.front(); } } } @@ -92,11 +94,15 @@ void Config::resolveConfigIncludes(Json::Value &config, int depth) { if (includes.isArray()) { for (const auto &include : includes) { spdlog::info("Including resource file: {}", include.asString()); - setupConfig(config, tryExpandPath(include.asString(), "").value_or(""), ++depth); + for (const auto &match : tryExpandPath(include.asString(), "")) { + setupConfig(config, match, depth + 1); + } } } else if (includes.isString()) { spdlog::info("Including resource file: {}", includes.asString()); - setupConfig(config, tryExpandPath(includes.asString(), "").value_or(""), ++depth); + for (const auto &match : tryExpandPath(includes.asString(), "")) { + setupConfig(config, match, depth + 1); + } } } diff --git a/src/factory.cpp b/src/factory.cpp index 6c2313e3..f7aa2d30 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -34,6 +34,7 @@ #include "modules/hyprland/language.hpp" #include "modules/hyprland/submap.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/windowcount.hpp" #include "modules/hyprland/workspaces.hpp" #endif #ifdef HAVE_NIRI @@ -41,6 +42,10 @@ #include "modules/niri/window.hpp" #include "modules/niri/workspaces.hpp" #endif +#ifdef HAVE_WAYFIRE +#include "modules/wayfire/window.hpp" +#include "modules/wayfire/workspaces.hpp" +#endif #if defined(__FreeBSD__) || defined(__linux__) #include "modules/battery.hpp" #endif @@ -109,6 +114,9 @@ #ifdef HAVE_SYSTEMD_MONITOR #include "modules/systemd_failed_units.hpp" #endif +#ifdef HAVE_LIBGPS +#include "modules/gps.hpp" +#endif #include "modules/cffi.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" @@ -140,7 +148,7 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, #endif #ifdef HAVE_PIPEWIRE if (ref == "privacy") { - return new waybar::modules::privacy::Privacy(id, config_[name], pos); + return new waybar::modules::privacy::Privacy(id, config_[name], bar_.orientation, pos); } #endif #ifdef HAVE_MPRIS @@ -201,6 +209,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "hyprland/window") { return new waybar::modules::hyprland::Window(id, bar_, config_[name]); } + if (ref == "hyprland/windowcount") { + return new waybar::modules::hyprland::WindowCount(id, bar_, config_[name]); + } if (ref == "hyprland/language") { return new waybar::modules::hyprland::Language(id, bar_, config_[name]); } @@ -221,6 +232,14 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "niri/workspaces") { return new waybar::modules::niri::Workspaces(id, bar_, config_[name]); } +#endif +#ifdef HAVE_WAYFIRE + if (ref == "wayfire/window") { + return new waybar::modules::wayfire::Window(id, bar_, config_[name]); + } + if (ref == "wayfire/workspaces") { + return new waybar::modules::wayfire::Workspaces(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); @@ -331,6 +350,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "systemd-failed-units") { return new waybar::modules::SystemdFailedUnits(id, config_[name]); } +#endif +#ifdef HAVE_LIBGPS + if (ref == "gps") { + return new waybar::modules::Gps(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/main.cpp b/src/main.cpp index 442c530c..6e7650a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -7,66 +8,115 @@ #include #include "client.hpp" +#include "util/SafeSignal.hpp" std::mutex reap_mtx; std::list reap; -volatile bool reload; -void* signalThread(void* args) { - int err; - int signum; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +static int signal_pipe_write_fd; + +// Write a single signal to `signal_pipe_write_fd`. +// This function is set as a signal handler, so it must be async-signal-safe. +static void writeSignalToPipe(int signum) { + ssize_t amt = write(signal_pipe_write_fd, &signum, sizeof(int)); + + // There's not much we can safely do inside of a signal handler. + // Let's just ignore any errors. + (void)amt; +} + +// This initializes `signal_pipe_write_fd`, and sets up signal handlers. +// +// This function will run forever, emitting every `SIGUSR1`, `SIGUSR2`, +// `SIGINT`, `SIGCHLD`, and `SIGRTMIN + 1`...`SIGRTMAX` signal received +// to `signal_handler`. +static void catchSignals(waybar::SafeSignal& signal_handler) { + int fd[2]; + pipe(fd); + + int signal_pipe_read_fd = fd[0]; + signal_pipe_write_fd = fd[1]; + + // This pipe should be able to buffer ~thousands of signals. If it fills up, + // we'll drop signals instead of blocking. + + // We can't allow the write end to block because we'll be writing to it in a + // signal handler, which could interrupt the loop that's reading from it and + // deadlock. + + fcntl(signal_pipe_write_fd, F_SETFL, O_NONBLOCK); + + std::signal(SIGUSR1, writeSignalToPipe); + std::signal(SIGUSR2, writeSignalToPipe); + std::signal(SIGINT, writeSignalToPipe); + std::signal(SIGCHLD, writeSignalToPipe); + + for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { + std::signal(sig, writeSignalToPipe); + } while (true) { - err = sigwait(&mask, &signum); - if (err != 0) { - spdlog::error("sigwait failed: {}", strerror(errno)); + int signum; + ssize_t amt = read(signal_pipe_read_fd, &signum, sizeof(int)); + if (amt < 0) { + spdlog::error("read from signal pipe failed with error {}, closing thread", strerror(errno)); + break; + } + + if (amt != sizeof(int)) { continue; } - switch (signum) { - case SIGCHLD: - spdlog::debug("Received SIGCHLD in signalThread"); - if (!reap.empty()) { - reap_mtx.lock(); - for (auto it = reap.begin(); it != reap.end(); ++it) { - if (waitpid(*it, nullptr, WNOHANG) == *it) { - spdlog::debug("Reaped child with PID: {}", *it); - it = reap.erase(it); - } - } - reap_mtx.unlock(); - } - break; - default: - spdlog::debug("Received signal with number {}, but not handling", signum); - break; - } + signal_handler.emit(signum); } } -void startSignalThread() { - int err; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +// Must be called on the main thread. +// +// If this signal should restart or close the bar, this function will write +// `true` or `false`, respectively, into `reload`. +static void handleSignalMainThread(int signum, bool& reload) { + if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { + for (auto& bar : waybar::Client::inst()->bars) { + bar->handleSignal(signum); + } - // Block SIGCHLD so it can be handled by the signal thread - // Any threads created by this one (the main thread) should not - // modify their signal mask to unblock SIGCHLD - err = pthread_sigmask(SIG_BLOCK, &mask, nullptr); - if (err != 0) { - spdlog::error("pthread_sigmask failed in startSignalThread: {}", strerror(err)); - exit(1); + return; } - pthread_t thread_id; - err = pthread_create(&thread_id, nullptr, signalThread, nullptr); - if (err != 0) { - spdlog::error("pthread_create failed in startSignalThread: {}", strerror(err)); - exit(1); + switch (signum) { + case SIGUSR1: + spdlog::debug("Visibility toggled"); + for (auto& bar : waybar::Client::inst()->bars) { + bar->toggle(); + } + break; + case SIGUSR2: + spdlog::info("Reloading..."); + reload = true; + waybar::Client::inst()->reset(); + break; + case SIGINT: + spdlog::info("Quitting."); + reload = false; + waybar::Client::inst()->reset(); + break; + case SIGCHLD: + spdlog::debug("Received SIGCHLD in signalThread"); + if (!reap.empty()) { + reap_mtx.lock(); + for (auto it = reap.begin(); it != reap.end(); ++it) { + if (waitpid(*it, nullptr, WNOHANG) == *it) { + spdlog::debug("Reaped child with PID: {}", *it); + it = reap.erase(it); + } + } + reap_mtx.unlock(); + } + break; + default: + spdlog::debug("Received signal with number {}, but not handling", signum); + break; } } @@ -74,32 +124,16 @@ 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(); - } - }); + bool reload; - std::signal(SIGUSR2, [](int /*signal*/) { - spdlog::info("Reloading..."); - reload = true; - waybar::Client::inst()->reset(); - }); + waybar::SafeSignal posix_signal_received; + posix_signal_received.connect([&](int signum) { handleSignalMainThread(signum, reload); }); - std::signal(SIGINT, [](int /*signal*/) { - spdlog::info("Quitting."); - reload = false; - waybar::Client::inst()->reset(); - }); + std::thread signal_thread([&]() { catchSignals(posix_signal_received); }); - for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { - std::signal(sig, [](int sig) { - for (auto& bar : waybar::Client::inst()->bars) { - bar->handleSignal(sig); - } - }); - } - startSignalThread(); + // Every `std::thread` must be joined or detached. + // This thread should run forever, so detach it. + signal_thread.detach(); auto ret = 0; do { @@ -109,7 +143,7 @@ int main(int argc, char* argv[]) { std::signal(SIGUSR1, SIG_IGN); std::signal(SIGUSR2, SIG_IGN); - std::signal(SIGINT, SIG_IGN); + std::signal(SIGINT, SIG_IGN); delete client; return ret; diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index d87cc612..3d0da330 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -273,14 +273,18 @@ waybar::modules::Battery::getInfos() { // Scale these by the voltage to get μW/μWh. uint32_t current_now = 0; + int32_t _current_now_int = 0; bool current_now_exists = false; if (fs::exists(bat / "current_now")) { current_now_exists = true; - std::ifstream(bat / "current_now") >> current_now; + std::ifstream(bat / "current_now") >> _current_now_int; } else if (fs::exists(bat / "current_avg")) { current_now_exists = true; - std::ifstream(bat / "current_avg") >> current_now; + std::ifstream(bat / "current_avg") >> _current_now_int; } + // Documentation ABI allows a negative value when discharging, positive + // value when charging. + current_now = std::abs(_current_now_int); if (fs::exists(bat / "time_to_empty_now")) { time_to_empty_now_exists = true; @@ -324,11 +328,15 @@ waybar::modules::Battery::getInfos() { } uint32_t power_now = 0; + int32_t _power_now_int = 0; bool power_now_exists = false; if (fs::exists(bat / "power_now")) { power_now_exists = true; - std::ifstream(bat / "power_now") >> power_now; + std::ifstream(bat / "power_now") >> _power_now_int; } + // Some drivers (example: Qualcomm) exposes use a negative value when + // discharging, positive value when charging. + power_now = std::abs(_power_now_int); uint32_t energy_now = 0; bool energy_now_exists = false; @@ -679,8 +687,11 @@ auto waybar::modules::Battery::update() -> void { 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; + if (time_remaining > 0) { + tooltip_text_default = std::string("Empty in ") + time_remaining_formatted; + } else { + tooltip_text_default = std::string("Full in ") + time_remaining_formatted; + } } else { tooltip_text_default = status_pretty; } @@ -693,7 +704,7 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text( + label_.set_tooltip_markup( fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default), fmt::arg("power", power), fmt::arg("capacity", capacity), fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles), diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c8f1f996..06475a2e 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -49,8 +49,8 @@ auto getBoolProperty(GDBusProxy* proxy, const char* property_name) -> bool { return false; } -auto getOptionalStringProperty(GDBusProxy* proxy, - const char* property_name) -> std::optional { +auto getOptionalStringProperty(GDBusProxy* proxy, const char* property_name) + -> std::optional { auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name); if (gvar) { std::string property_value = g_variant_get_string(gvar, NULL); @@ -345,8 +345,8 @@ auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* m auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged( GDBusObjectManagerClient* manager, GDBusObjectProxy* object_proxy, GDBusProxy* interface_proxy, - GVariant* changed_properties, const gchar* const* invalidated_properties, - gpointer user_data) -> void { + GVariant* changed_properties, const gchar* const* invalidated_properties, gpointer user_data) + -> void { std::string interface_name = g_dbus_proxy_get_interface_name(interface_proxy); std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy)); @@ -395,8 +395,8 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) return std::nullopt; } -auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, - DeviceInfo& device_info) -> bool { +auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info) + -> bool { GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1")); if (proxy_device != NULL) { @@ -462,8 +462,9 @@ auto waybar::modules::Bluetooth::findCurController() -> std::optional& connected_devices) -> void { +auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_controller_path, + std::vector& connected_devices) + -> void { GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp index f16d3f63..405a351a 100644 --- a/src/modules/cava.cpp +++ b/src/modules/cava.cpp @@ -139,7 +139,7 @@ auto waybar::modules::Cava::update() -> void { } } - if (silence_ && prm_.sleep_timer) { + if (silence_ && prm_.sleep_timer != 0) { if (sleep_counter_ <= (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { ++sleep_counter_; @@ -147,7 +147,7 @@ auto waybar::modules::Cava::update() -> void { } } - if (!silence_) { + if (!silence_ || prm_.sleep_timer == 0) { downThreadDelay(frame_time_milsec_, suspend_silence_delay_); // Process: execute cava pthread_mutex_lock(&audio_data_.lock); diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index e560659b..5c095f46 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -28,7 +28,7 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co } // Fetch functions - if (*wbcffi_version == 1) { + if (*wbcffi_version == 1 || *wbcffi_version == 2) { // Mandatory functions hooks_.init = reinterpret_cast(dlsym(handle, "wbcffi_init")); if (!hooks_.init) { @@ -58,10 +58,14 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co const auto& keys = config.getMemberNames(); for (size_t i = 0; i < keys.size(); i++) { const auto& value = config[keys[i]]; - if (value.isConvertibleTo(Json::ValueType::stringValue)) { - config_entries_stringstor.push_back(config[keys[i]].asString()); + if (*wbcffi_version == 1) { + if (value.isConvertibleTo(Json::ValueType::stringValue)) { + config_entries_stringstor.push_back(value.asString()); + } else { + config_entries_stringstor.push_back(value.toStyledString()); + } } else { - config_entries_stringstor.push_back(config[keys[i]].toStyledString()); + config_entries_stringstor.push_back(value.toStyledString()); } } diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 7f5a4d55..a7d57437 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -1,5 +1,6 @@ #include "modules/clock.hpp" +#include #include #include @@ -16,6 +17,7 @@ #include #endif +using namespace date; namespace fmt_lib = waybar::util::date::format; waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) @@ -25,6 +27,7 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) m_tooltip_{new Gtk::Label()}, cldInTooltip_{m_tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, cldYearShift_{January / 1 / 1900}, + cldMonShift_{year(1900) / January}, tzInTooltip_{m_tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, tzCurrIdx_{0}, ordInTooltip_{m_tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { @@ -199,8 +202,8 @@ const unsigned cldRowsInMonth(const year_month& ym, const weekday& firstdow) { return 2u + ceil((weekday{ym / 1} - firstdow) + ((ym / last).day() - day{0})).count(); } -auto cldGetWeekForLine(const year_month& ym, const weekday& firstdow, - const unsigned line) -> const year_month_weekday { +auto cldGetWeekForLine(const year_month& ym, const weekday& firstdow, const unsigned line) + -> const year_month_weekday { unsigned index{line - 2}; if (weekday{ym / 1} == firstdow) ++index; return ym / firstdow[index]; @@ -348,9 +351,9 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea m_locale_, fmtMap_[4], fmt_lib::make_format_args( (line == 2) - ? static_cast( + ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast(zoned_seconds{ + : static_cast(zoned_seconds{ tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}}))) << ' '; } else @@ -358,10 +361,23 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea } } - os << Glib::ustring::format((cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, - std::setfill(L' '), - std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0)), - getCalendarLine(today, ymTmp, line, firstdow, &m_locale_)); + // Count wide characters to avoid extra padding + size_t wideCharCount = 0; + std::string calendarLine = getCalendarLine(today, ymTmp, line, firstdow, &m_locale_); + if (line < 2) { + for (gchar *data = calendarLine.data(), *end = data + calendarLine.size(); + data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (g_unichar_iswide(c)) { + wideCharCount++; + } + data = g_utf8_find_next_char(data, end); + } + } + os << Glib::ustring::format( + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), + calendarLine); // Week numbers on the right if (cldWPos_ == WS::RIGHT && line > 0) { @@ -371,9 +387,9 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea << fmt_lib::vformat( m_locale_, fmtMap_[4], fmt_lib::make_format_args( - (line == 2) ? static_cast( + (line == 2) ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast( + : static_cast( zoned_seconds{tz, local_days{cldGetWeekForLine( ymTmp, firstdow, line)}}))); else diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index e023aaf6..db5c6db3 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -35,6 +35,13 @@ waybar::modules::Custom::~Custom() { void waybar::modules::Custom::delayWorker() { thread_ = [this] { + for (int i : this->pid_children_) { + int status; + waitpid(i, &status, 0); + } + + this->pid_children_.clear(); + bool can_update = true; if (config_["exec-if"].isString()) { output_ = util::command::execNoRead(config_["exec-if"].asString()); @@ -62,7 +69,7 @@ void waybar::modules::Custom::continuousWorker() { } thread_ = [this, cmd] { char* buff = nullptr; - waybar::util::ScopeGuard buff_deleter([buff]() { + waybar::util::ScopeGuard buff_deleter([&buff]() { if (buff) { free(buff); } diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 870d87e4..a960a1f0 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -9,6 +9,7 @@ #include "client.hpp" #include "dwl-ipc-unstable-v2-client-protocol.h" +#include "glibmm/markup.h" #include "util/rewrite_string.hpp" namespace waybar::modules::dwl { @@ -97,11 +98,13 @@ Window::~Window() { } } -void Window::handle_title(const char *title) { title_ = title; } +void Window::handle_title(const char *title) { title_ = Glib::Markup::escape_text(title); } -void Window::handle_appid(const char *appid) { appid_ = appid; } +void Window::handle_appid(const char *appid) { appid_ = Glib::Markup::escape_text(appid); } -void Window::handle_layout_symbol(const char *layout_symbol) { layout_symbol_ = layout_symbol; } +void Window::handle_layout_symbol(const char *layout_symbol) { + layout_symbol_ = Glib::Markup::escape_text(layout_symbol); +} void Window::handle_layout(const uint32_t layout) { layout_ = layout; } diff --git a/src/modules/gps.cpp b/src/modules/gps.cpp new file mode 100644 index 00000000..a7bab659 --- /dev/null +++ b/src/modules/gps.cpp @@ -0,0 +1,216 @@ +#include "modules/gps.hpp" + +#include +#include + +#include +#include + +// 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 + +namespace { +using namespace waybar::util; +constexpr const char* DEFAULT_FORMAT = "{mode}"; +} // namespace + +waybar::modules::Gps::Gps(const std::string& id, const Json::Value& config) + : ALabel(config, "gps", id, "{}", 5) +#ifdef WANT_RFKILL + , + rfkill_{RFKILL_TYPE_GPS} +#endif +{ + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; + + if (0 != gps_open("localhost", "2947", &gps_data_)) { + throw std::runtime_error("Can't open gpsd socket"); + } + + if (config_["hide-disconnected"].isBool()) { + hideDisconnected = config_["hide-disconnected"].asBool(); + } + + if (config_["hide-no-fix"].isBool()) { + hideNoFix = config_["hide-no-fix"].asBool(); + } + + gps_thread_ = [this] { + dp.emit(); + gps_stream(&gps_data_, WATCH_ENABLE, NULL); + int last_gps_mode = 0; + + while (gps_waiting(&gps_data_, 5000000)) { + if (gps_read(&gps_data_, NULL, 0) == -1) { + throw std::runtime_error("Can't read data from gpsd."); + } + + if (MODE_SET != (MODE_SET & gps_data_.set)) { + // did not even get mode, nothing to see here + continue; + } + + if (gps_data_.fix.mode != last_gps_mode) { + // significant update + dp.emit(); + } + last_gps_mode = gps_data_.fix.mode; + } + }; + +#ifdef WANT_RFKILL + rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Gps::update))); +#endif +} + +const std::string waybar::modules::Gps::getFixModeName() const { + switch (gps_data_.fix.mode) { + case MODE_NO_FIX: + return "fix-none"; + case MODE_2D: + return "fix-2d"; + case MODE_3D: + return "fix-3d"; + default: +#ifdef WANT_RFKILL + if (rfkill_.getState()) return "disabled"; +#endif + return "disconnected"; + } +} + +const std::string waybar::modules::Gps::getFixModeString() const { + switch (gps_data_.fix.mode) { + case MODE_NO_FIX: + return "No fix"; + case MODE_2D: + return "2D Fix"; + case MODE_3D: + return "3D Fix"; + default: + return "Disconnected"; + } +} + +const std::string waybar::modules::Gps::getFixStatusString() const { + switch (gps_data_.fix.status) { + case STATUS_GPS: + return "GPS"; + case STATUS_DGPS: + return "DGPS"; + case STATUS_RTK_FIX: + return "RTK Fixed"; + case STATUS_RTK_FLT: + return "RTK Float"; + case STATUS_DR: + return "Dead Reckoning"; + case STATUS_GNSSDR: + return "GNSS + Dead Reckoning"; + case STATUS_TIME: + return "Time Only"; + case STATUS_PPS_FIX: + return "PPS Fix"; + default: + +#ifdef WANT_RFKILL + if (rfkill_.getState()) return "Disabled"; +#endif + + return "Unknown"; + } +} + +auto waybar::modules::Gps::update() -> void { + sleep(0); // Wait for gps status change + + if ((gps_data_.fix.mode == MODE_NOT_SEEN && hideDisconnected) || + (gps_data_.fix.mode == MODE_NO_FIX && hideNoFix)) { + event_box_.set_visible(false); + return; + } + + // Show the module + if (!event_box_.get_visible()) event_box_.set_visible(true); + + std::string tooltip_format; + + if (!alt_) { + auto state = getFixModeName(); + if (!state_.empty() && label_.get_style_context()->has_class(state_)) { + label_.get_style_context()->remove_class(state_); + } + 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(); + } + if (!label_.get_style_context()->has_class(state)) { + label_.get_style_context()->add_class(state); + } + format_ = default_format_; + state_ = state; + } + + auto format = format_; + + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("mode", getFixModeString())); + store.push_back(fmt::arg("status", getFixStatusString())); + + store.push_back(fmt::arg("latitude", gps_data_.fix.latitude)); + store.push_back(fmt::arg("latitude_error", gps_data_.fix.epy)); + + store.push_back(fmt::arg("longitude", gps_data_.fix.longitude)); + store.push_back(fmt::arg("longitude_error", gps_data_.fix.epx)); + + store.push_back(fmt::arg("altitude_hae", gps_data_.fix.altHAE)); + store.push_back(fmt::arg("altitude_msl", gps_data_.fix.altMSL)); + store.push_back(fmt::arg("altitude_error", gps_data_.fix.epv)); + + store.push_back(fmt::arg("speed", gps_data_.fix.speed)); + store.push_back(fmt::arg("speed_error", gps_data_.fix.eps)); + + store.push_back(fmt::arg("climb", gps_data_.fix.climb)); + store.push_back(fmt::arg("climb_error", gps_data_.fix.epc)); + + store.push_back(fmt::arg("satellites_used", gps_data_.satellites_used)); + store.push_back(fmt::arg("satellites_visible", gps_data_.satellites_visible)); + + auto text = fmt::vformat(format, store); + + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + auto tooltip_text = fmt::vformat(tooltip_format, store); + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else if (label_.get_tooltip_text() != text) { + label_.set_tooltip_markup(text); + } + } + label_.set_markup(text); + // Call parent update + ALabel::update(); +} + +waybar::modules::Gps::~Gps() { + gps_stream(&gps_data_, WATCH_DISABLE, NULL); + gps_close(&gps_data_); +} diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 39341a14..2bd3b509 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -11,7 +11,6 @@ #include #include -#include namespace waybar::modules::hyprland { @@ -44,71 +43,96 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { return socketFolder_; } -void IPC::startIPC() { +IPC::IPC() { // will start IPC and relay events to parseIPC + ipcThread_ = std::thread([this]() { socketListener(); }); +} - std::thread([&]() { - // check for hyprland - const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - - if (his == nullptr) { - spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); - return; +IPC::~IPC() { + running_ = false; + spdlog::info("Hyprland IPC stopping..."); + if (socketfd_ != -1) { + spdlog::trace("Shutting down socket"); + if (shutdown(socketfd_, SHUT_RDWR) == -1) { + spdlog::error("Hyprland IPC: Couldn't shutdown socket"); } - - if (!modulesReady) return; - - spdlog::info("Hyprland IPC starting"); - - struct sockaddr_un addr; - int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); - - if (socketfd == -1) { - spdlog::error("Hyprland IPC: socketfd failed"); - return; + spdlog::trace("Closing socket"); + if (close(socketfd_) == -1) { + spdlog::error("Hyprland IPC: Couldn't close socket"); } + } + ipcThread_.join(); +} - addr.sun_family = AF_UNIX; +IPC& IPC::inst() { + static IPC ipc; + return ipc; +} - auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; - strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); +void IPC::socketListener() { + // check for hyprland + const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + if (his == nullptr) { + spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); + return; + } - int l = sizeof(struct sockaddr_un); + if (!modulesReady) return; - if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { - spdlog::error("Hyprland IPC: Unable to connect?"); - return; - } + spdlog::info("Hyprland IPC starting"); - auto* file = fdopen(socketfd, "r"); + struct sockaddr_un addr; + socketfd_ = socket(AF_UNIX, SOCK_STREAM, 0); - while (true) { - std::array buffer; // Hyprland socket2 events are max 1024 bytes + if (socketfd_ == -1) { + spdlog::error("Hyprland IPC: socketfd failed"); + return; + } - auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); + addr.sun_family = AF_UNIX; - if (receivedCharPtr == nullptr) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; - } + auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; + strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); - std::string messageReceived(buffer.data()); - messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); - spdlog::debug("hyprland IPC received {}", messageReceived); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; - try { - parseIPC(messageReceived); - } catch (std::exception& e) { - spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); - } catch (...) { - throw; - } + int l = sizeof(struct sockaddr_un); + if (connect(socketfd_, (struct sockaddr*)&addr, l) == -1) { + spdlog::error("Hyprland IPC: Unable to connect?"); + return; + } + auto* file = fdopen(socketfd_, "r"); + if (file == nullptr) { + spdlog::error("Hyprland IPC: Couldn't open file descriptor"); + return; + } + while (running_) { + std::array buffer; // Hyprland socket2 events are max 1024 bytes + + auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); + + if (receivedCharPtr == nullptr) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; } - }).detach(); + + std::string messageReceived(buffer.data()); + messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); + spdlog::debug("hyprland IPC received {}", messageReceived); + + try { + parseIPC(messageReceived); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); + } catch (...) { + throw; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + spdlog::debug("Hyprland IPC stopped"); } void IPC::parseIPC(const std::string& ev) { diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index d86393af..3f141bbd 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -10,13 +10,9 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { modulesReady = true; - if (!gIPC) { - gIPC = std::make_unique(); - } - // get the active layout when open initLanguage(); @@ -24,11 +20,11 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con update(); // register for hyprland ipc - gIPC->registerForIPC("activelayout", this); + m_ipc.registerForIPC("activelayout", this); } Language::~Language() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(mutex_); } @@ -70,7 +66,18 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); - auto layoutName = ev.substr(ev.find_last_of(',') + 1); + + // Last comma before variants parenthesis, eg: + // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., + // with dead keys) + std::string beforeParenthesis; + auto parenthesisPos = ev.find_last_of('('); + if (parenthesisPos == std::string::npos) { + beforeParenthesis = ev; + } else { + beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos); + } + auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1); if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore @@ -85,7 +92,7 @@ void Language::onEvent(const std::string& ev) { } void Language::initLanguage() { - const auto inputDevices = gIPC->getSocket1Reply("devices"); + const auto inputDevices = m_ipc.getSocket1Reply("devices"); const auto kbName = config_["keyboard-name"].asString(); diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index 96677d12..a2b3f460 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -7,32 +7,28 @@ namespace waybar::modules::hyprland { Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "submap", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "submap", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { modulesReady = true; parseConfig(config); - if (!gIPC) { - gIPC = std::make_unique(); - } - label_.hide(); ALabel::update(); // Displays widget immediately if always_on_ assuming default submap - // Needs an actual way to retrive current submap on startup + // Needs an actual way to retrieve current submap on startup if (always_on_) { submap_ = default_submap_; label_.get_style_context()->add_class(submap_); } // register for hyprland ipc - gIPC->registerForIPC("submap", this); + m_ipc.registerForIPC("submap", this); dp.emit(); } Submap::~Submap() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(mutex_); } @@ -72,8 +68,7 @@ void Submap::onEvent(const std::string& ev) { return; } - auto submapName = ev.substr(ev.find_last_of('>') + 1); - submapName = waybar::util::sanitize_string(submapName); + auto submapName = ev.substr(ev.find_first_of('>') + 2); if (!submap_.empty()) { label_.get_style_context()->remove_class(submap_); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index b5ed8f02..815fbad8 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include "modules/hyprland/backend.hpp" @@ -14,54 +15,72 @@ namespace waybar::modules::hyprland { +std::shared_mutex windowIpcSmtx; + Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) - : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) { + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); + modulesReady = true; separateOutputs_ = config["separate-outputs"].asBool(); - if (!gIPC) { - gIPC = std::make_unique(); - } + // register for hyprland ipc + m_ipc.registerForIPC("activewindow", this); + m_ipc.registerForIPC("closewindow", this); + m_ipc.registerForIPC("movewindow", this); + m_ipc.registerForIPC("changefloatingmode", this); + m_ipc.registerForIPC("fullscreen", this); + + windowIpcUniqueLock.unlock(); queryActiveWorkspace(); update(); dp.emit(); - - // register for hyprland ipc - gIPC->registerForIPC("activewindow", this); - gIPC->registerForIPC("closewindow", this); - gIPC->registerForIPC("movewindow", this); - gIPC->registerForIPC("changefloatingmode", this); - gIPC->registerForIPC("fullscreen", this); } Window::~Window() { - gIPC->unregisterForIPC(this); - // wait for possible event handler to finish - std::lock_guard lg(mutex_); + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); + m_ipc.unregisterForIPC(this); } auto Window::update() -> void { - // fix ampersands - std::lock_guard lg(mutex_); + std::shared_lock windowIpcShareLock(windowIpcSmtx); std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title); std::string windowAddress = workspace_.last_window; windowData_.title = windowName; + std::string label_text; if (!format_.empty()) { label_.show(); - label_.set_markup(waybar::util::rewriteString( + label_text = waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name)), - config_["rewrite"])); + config_["rewrite"]); + label_.set_markup(label_text); } else { label_.hide(); } + if (tooltipEnabled()) { + std::string tooltip_format; + if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_text( + fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name))); + } else if (!label_text.empty()) { + label_.set_tooltip_text(label_text); + } + } + if (focused_) { image_.show(); } else { @@ -91,7 +110,7 @@ auto Window::update() -> void { } auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); + const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace"); if (workspace.isObject()) { return Workspace::parse(workspace); @@ -101,24 +120,33 @@ auto Window::getActiveWorkspace() -> Workspace { } auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { - const auto monitors = gIPC->getSocket1JsonReply("monitors"); + const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); if (monitors.isArray()) { - auto monitor = std::find_if(monitors.begin(), monitors.end(), [&](Json::Value monitor) { - return monitor["name"] == monitorName; - }); + auto monitor = std::ranges::find_if( + monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); - return Workspace{-1, 0, "", ""}; + return Workspace{ + .id = -1, + .windows = 0, + .last_window = "", + .last_window_title = "", + }; } const int id = (*monitor)["activeWorkspace"]["id"].asInt(); - const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); + const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces"); if (workspaces.isArray()) { - auto workspace = std::find_if(workspaces.begin(), workspaces.end(), - [&](Json::Value workspace) { return workspace["id"] == id; }); + auto workspace = std::ranges::find_if( + workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); if (workspace == std::end(workspaces)) { spdlog::warn("No workspace with id {}", id); - return Workspace{-1, 0, "", ""}; + return Workspace{ + .id = -1, + .windows = 0, + .last_window = "", + .last_window_title = "", + }; } return Workspace::parse(*workspace); }; @@ -129,22 +157,26 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { auto Window::Workspace::parse(const Json::Value& value) -> Window::Workspace { return Workspace{ - value["id"].asInt(), - value["windows"].asInt(), - value["lastwindow"].asString(), - value["lastwindowtitle"].asString(), + .id = value["id"].asInt(), + .windows = value["windows"].asInt(), + .last_window = value["lastwindow"].asString(), + .last_window_title = value["lastwindowtitle"].asString(), }; } auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { - return WindowData{value["floating"].asBool(), value["monitor"].asInt(), - value["class"].asString(), value["initialClass"].asString(), - value["title"].asString(), value["initialTitle"].asString(), - value["fullscreen"].asBool(), !value["grouped"].empty()}; + return WindowData{.floating = value["floating"].asBool(), + .monitor = value["monitor"].asInt(), + .class_name = value["class"].asString(), + .initial_class_name = value["initialClass"].asString(), + .title = value["title"].asString(), + .initial_title = value["initialTitle"].asString(), + .fullscreen = value["fullscreen"].asBool(), + .grouped = !value["grouped"].empty()}; } void Window::queryActiveWorkspace() { - std::lock_guard lg(mutex_); + std::shared_lock windowIpcShareLock(windowIpcSmtx); if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); @@ -154,11 +186,10 @@ void Window::queryActiveWorkspace() { focused_ = true; if (workspace_.windows > 0) { - const auto clients = gIPC->getSocket1JsonReply("clients"); + const auto clients = m_ipc.getSocket1JsonReply("clients"); if (clients.isArray()) { - auto activeWindow = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { - return window["address"] == workspace_.last_window; - }); + auto activeWindow = std::ranges::find_if( + clients, [&](Json::Value window) { return window["address"] == workspace_.last_window; }); if (activeWindow == std::end(clients)) { focused_ = false; @@ -168,22 +199,19 @@ void Window::queryActiveWorkspace() { windowData_ = WindowData::parse(*activeWindow); updateAppIconName(windowData_.class_name, windowData_.initial_class_name); std::vector workspaceWindows; - std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspaceWindows), - [&](Json::Value window) { - return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); - }); - swallowing_ = - std::any_of(workspaceWindows.begin(), workspaceWindows.end(), [&](Json::Value window) { - return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; - }); + std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](Json::Value window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); + }); + swallowing_ = std::ranges::any_of(workspaceWindows, [&](Json::Value window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); std::vector visibleWindows; - std::copy_if(workspaceWindows.begin(), workspaceWindows.end(), - std::back_inserter(visibleWindows), - [&](Json::Value window) { return !window["hidden"].asBool(); }); + std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), + [&](Json::Value window) { return !window["hidden"].asBool(); }); solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), [&](Json::Value window) { return !window["floating"].asBool(); }); - allFloating_ = std::all_of(visibleWindows.begin(), visibleWindows.end(), - [&](Json::Value window) { return window["floating"].asBool(); }); + allFloating_ = std::ranges::all_of( + visibleWindows, [&](Json::Value window) { return window["floating"].asBool(); }); fullscreen_ = windowData_.fullscreen; // Fullscreen windows look like they are solo @@ -191,18 +219,12 @@ void Window::queryActiveWorkspace() { solo_ = true; } - // Grouped windows have a tab bar and therefore don't look fullscreen or solo - if (windowData_.grouped) { - fullscreen_ = false; - solo_ = false; - } - if (solo_) { soloClass_ = windowData_.class_name; } else { soloClass_ = ""; } - }; + } } else { focused_ = false; windowData_ = WindowData{}; diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp new file mode 100644 index 00000000..68f7c3b4 --- /dev/null +++ b/src/modules/hyprland/windowcount.cpp @@ -0,0 +1,142 @@ +#include "modules/hyprland/windowcount.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "modules/hyprland/backend.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::hyprland { + +WindowCount::WindowCount(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "windowcount", id, "{count}", 0, true), bar_(bar) { + modulesReady = true; + separateOutputs_ = + config.isMember("separate-outputs") ? config["separate-outputs"].asBool() : true; + + if (!gIPC) { + gIPC = std::make_unique(); + } + + queryActiveWorkspace(); + update(); + dp.emit(); + + // register for hyprland ipc + gIPC->registerForIPC("fullscreen", this); + gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("focusedmon", this); + gIPC->registerForIPC("openwindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); +} + +WindowCount::~WindowCount() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +auto WindowCount::update() -> void { + std::lock_guard lg(mutex_); + + std::string format = config_["format"].asString(); + std::string formatEmpty = config_["format-empty"].asString(); + std::string formatWindowed = config_["format-windowed"].asString(); + std::string formatFullscreen = config_["format-fullscreen"].asString(); + + setClass("empty", workspace_.windows == 0); + setClass("fullscreen", workspace_.hasfullscreen); + + if (workspace_.windows == 0 && !formatEmpty.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatEmpty), workspace_.windows)); + } else if (!workspace_.hasfullscreen && !formatWindowed.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatWindowed), workspace_.windows)); + } else if (workspace_.hasfullscreen && !formatFullscreen.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatFullscreen), workspace_.windows)); + } else if (!format.empty()) { + label_.set_markup(fmt::format(fmt::runtime(format), workspace_.windows)); + } else { + label_.set_text(fmt::format("{}", workspace_.windows)); + } + + label_.show(); + AAppIconLabel::update(); +} + +auto WindowCount::getActiveWorkspace() -> Workspace { + const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); + + if (workspace.isObject()) { + return Workspace::parse(workspace); + } + + return {}; +} + +auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspace { + const auto monitors = gIPC->getSocket1JsonReply("monitors"); + if (monitors.isArray()) { + auto monitor = std::find_if(monitors.begin(), monitors.end(), [&](Json::Value monitor) { + return monitor["name"] == monitorName; + }); + if (monitor == std::end(monitors)) { + spdlog::warn("Monitor not found: {}", monitorName); + return Workspace{-1, 0, false}; + } + const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + + const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); + if (workspaces.isArray()) { + auto workspace = std::find_if(workspaces.begin(), workspaces.end(), + [&](Json::Value workspace) { return workspace["id"] == id; }); + if (workspace == std::end(workspaces)) { + spdlog::warn("No workspace with id {}", id); + return Workspace{-1, 0, false}; + } + return Workspace::parse(*workspace); + }; + }; + + return {}; +} + +auto WindowCount::Workspace::parse(const Json::Value& value) -> WindowCount::Workspace { + return Workspace{ + value["id"].asInt(), + value["windows"].asInt(), + value["hasfullscreen"].asBool(), + }; +} + +void WindowCount::queryActiveWorkspace() { + std::lock_guard lg(mutex_); + + if (separateOutputs_) { + workspace_ = getActiveWorkspace(this->bar_.output->name); + } else { + workspace_ = getActiveWorkspace(); + } +} + +void WindowCount::onEvent(const std::string& ev) { + queryActiveWorkspace(); + dp.emit(); +} + +void WindowCount::setClass(const std::string& classname, bool enable) { + if (enable) { + if (!bar_.window.get_style_context()->has_class(classname)) { + bar_.window.get_style_context()->add_class(classname); + } + } else { + bar_.window.get_style_context()->remove_class(classname); + } +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp index df7fe784..2a62ac12 100644 --- a/src/modules/hyprland/windowcreationpayload.cpp +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -88,7 +88,7 @@ bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } -void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { +void WindowCreationPayload::moveToWorkspace(std::string &new_workspace_name) { m_workspaceName = new_workspace_name; } diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index e575d1c4..b58c1719 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -18,7 +18,8 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma m_windows(workspace_data["windows"].asInt()), m_isActive(true), m_isPersistentRule(workspace_data["persistent-rule"].asBool()), - m_isPersistentConfig(workspace_data["persistent-config"].asBool()) { + m_isPersistentConfig(workspace_data["persistent-config"].asBool()), + m_ipc(IPC::inst()) { if (m_name.starts_with("name:")) { m_name = m_name.substr(5); } else if (m_name.starts_with("special")) { @@ -58,20 +59,20 @@ bool Workspace::handleClicked(GdkEventButton *bt) const { try { if (id() > 0) { // normal if (m_workspaceManager.moveToMonitor()) { - gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); } else { - gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); + m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); } } else if (!isSpecial()) { // named (this includes persistent) if (m_workspaceManager.moveToMonitor()) { - gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); } else { - gIPC->getSocket1Reply("dispatch workspace name:" + name()); + m_ipc.getSocket1Reply("dispatch workspace name:" + name()); } } else if (id() != -99) { // named special - gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); + m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); } else { // special - gIPC->getSocket1Reply("dispatch togglespecialworkspace"); + m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); } return true; } catch (const std::exception &e) { @@ -90,19 +91,19 @@ void Workspace::initializeWindowMap(const Json::Value &clients_data) { } } -void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { - if (!create_window_paylod.isEmpty(m_workspaceManager)) { - auto repr = create_window_paylod.repr(m_workspaceManager); +void Workspace::insertWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(m_workspaceManager)) { + auto repr = create_window_payload.repr(m_workspaceManager); if (!repr.empty()) { - m_windowMap[create_window_paylod.getAddress()] = repr; + m_windowMap[create_window_payload.getAddress()] = repr; } } }; -bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { - if (create_window_paylod.getWorkspaceName() == name()) { - insertWindow(create_window_paylod); +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payload) { + if (create_window_payload.getWorkspaceName() == name()) { + insertWindow(create_window_payload); return true; } return false; @@ -172,6 +173,10 @@ std::string &Workspace::selectIcon(std::map &icons_map } void Workspace::update(const std::string &format, const std::string &icon) { + if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) { + m_button.hide(); + return; + } // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 13364f3f..06a6ed3c 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -13,7 +14,10 @@ namespace waybar::modules::hyprland { Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) - : AModule(config, "workspaces", id, false, false), m_bar(bar), m_box(bar.orientation, 0) { + : AModule(config, "workspaces", id, false, false), + m_bar(bar), + m_box(bar.orientation, 0), + m_ipc(IPC::inst()) { modulesReady = true; parseConfig(config); @@ -24,23 +28,19 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value m_box.get_style_context()->add_class(MODULE_CLASS); event_box_.add(m_box); - if (!gIPC) { - gIPC = std::make_unique(); - } - setCurrentMonitorId(); init(); registerIpc(); } Workspaces::~Workspaces() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); } void Workspaces::init() { - m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + m_activeWorkspaceId = m_ipc.getSocket1JsonReply("activeworkspace")["id"].asInt(); initializeWorkspaces(); dp.emit(); @@ -50,13 +50,12 @@ Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, std::string const &monitor) { spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); Json::Value workspaceData; - try { - // numbered persistent workspaces get the name as ID - workspaceData["id"] = name == "special" ? -99 : std::stoi(name); - } catch (const std::exception &e) { - // named persistent workspaces start with ID=0 - workspaceData["id"] = 0; + + auto workspaceId = parseWorkspaceId(name); + if (!workspaceId.has_value()) { + workspaceId = 0; } + workspaceData["id"] = *workspaceId; workspaceData["name"] = name; workspaceData["monitor"] = monitor; workspaceData["windows"] = 0; @@ -66,12 +65,15 @@ Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, void Workspaces::createWorkspace(Json::Value const &workspace_data, Json::Value const &clients_data) { auto workspaceName = workspace_data["name"].asString(); + auto workspaceId = workspace_data["id"].asInt(); spdlog::debug("Creating workspace {}", workspaceName); // avoid recreating existing workspaces - auto workspace = std::find_if( - m_workspaces.begin(), m_workspaces.end(), - [workspaceName](std::unique_ptr const &w) { + auto workspace = + std::ranges::find_if(m_workspaces, [&](std::unique_ptr const &w) { + if (workspaceId > 0) { + return w->id() == workspaceId; + } return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || workspaceName == w->name(); }); @@ -81,14 +83,14 @@ void Workspaces::createWorkspace(Json::Value const &workspace_data, const auto keys = workspace_data.getMemberNames(); const auto *k = "persistent-rule"; - if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + if (std::ranges::find(keys, k) != keys.end()) { spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, workspace_data[k].asBool() ? "true" : "false"); (*workspace)->setPersistentRule(workspace_data[k].asBool()); } k = "persistent-config"; - if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + if (std::ranges::find(keys, k) != keys.end()) { spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, workspace_data[k].asBool() ? "true" : "false"); (*workspace)->setPersistentConfig(workspace_data[k].asBool()); @@ -159,18 +161,18 @@ std::string Workspaces::getRewrite(std::string window_class, std::string window_ fmt::arg("title", window_title)); } -std::vector Workspaces::getVisibleWorkspaces() { - std::vector visibleWorkspaces; - auto monitors = gIPC->getSocket1JsonReply("monitors"); +std::vector Workspaces::getVisibleWorkspaces() { + std::vector visibleWorkspaces; + auto monitors = IPC::inst().getSocket1JsonReply("monitors"); for (const auto &monitor : monitors) { auto ws = monitor["activeWorkspace"]; - if (ws.isObject() && ws["name"].isString()) { - visibleWorkspaces.push_back(ws["name"].asString()); + if (ws.isObject() && ws["id"].isInt()) { + visibleWorkspaces.push_back(ws["id"].asInt()); } auto sws = monitor["specialWorkspace"]; auto name = sws["name"].asString(); - if (sws.isObject() && sws["name"].isString() && !name.empty()) { - visibleWorkspaces.push_back(!name.starts_with("special:") ? name : name.substr(8)); + if (sws.isObject() && sws["id"].isInt() && !name.empty()) { + visibleWorkspaces.push_back(sws["id"].asInt()); } } return visibleWorkspaces; @@ -181,12 +183,12 @@ void Workspaces::initializeWorkspaces() { // if the workspace rules changed since last initialization, make sure we reset everything: for (auto &workspace : m_workspaces) { - m_workspacesToRemove.push_back(workspace->name()); + m_workspacesToRemove.push_back(std::to_string(workspace->id())); } // get all current workspaces - auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - auto const clientsJson = gIPC->getSocket1JsonReply("clients"); + auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); + auto const clientsJson = m_ipc.getSocket1JsonReply("clients"); for (Json::Value workspaceJson : workspacesJson) { std::string workspaceName = workspaceJson["name"].asString(); @@ -233,7 +235,7 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs std::vector persistentWorkspacesToCreate; const std::string currentMonitor = m_bar.output->name; - const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); + const bool monitorInConfig = std::ranges::find(keys, currentMonitor) != keys.end(); for (const std::string &key : keys) { // only add if either: // 1. key is the current monitor name @@ -248,17 +250,15 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs int amount = value.asInt(); spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); for (int i = 0; i < amount; i++) { - persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); + persistentWorkspacesToCreate.emplace_back(std::to_string((m_monitorId * amount) + i + 1)); } } } else if (value.isArray() && !value.empty()) { // value is an array => create defined workspaces for this monitor if (canCreate) { for (const Json::Value &workspace : value) { - if (workspace.isInt()) { - spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); - persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); - } + spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); + persistentWorkspacesToCreate.emplace_back(workspace.asString()); } } else { // key is the workspace and value is array of monitors to create on @@ -285,7 +285,7 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); - auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); + auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); for (Json::Value const &rule : workspaceRules) { if (!rule["workspaceString"].isString()) { spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); @@ -294,7 +294,8 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c if (!rule["persistent"].asBool()) { continue; } - auto const &workspace = rule["workspaceString"].asString(); + auto const &workspace = rule.isMember("defaultName") ? rule["defaultName"].asString() + : rule["workspaceString"].asString(); auto const &monitor = rule["monitor"].asString(); // create this workspace persistently if: // 1. the allOutputs config option is enabled @@ -306,6 +307,7 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c workspaceData["persistent-rule"] = true; m_workspacesToCreate.emplace_back(workspaceData, clientsJson); } else { + // This can be any workspace selector. m_workspacesToRemove.emplace_back(workspace); } } @@ -316,29 +318,29 @@ void Workspaces::onEvent(const std::string &ev) { std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); std::string payload = ev.substr(eventName.size() + 2); - if (eventName == "workspace") { + if (eventName == "workspacev2") { onWorkspaceActivated(payload); } else if (eventName == "activespecial") { onSpecialWorkspaceActivated(payload); - } else if (eventName == "destroyworkspace") { + } else if (eventName == "destroyworkspacev2") { onWorkspaceDestroyed(payload); - } else if (eventName == "createworkspace") { + } else if (eventName == "createworkspacev2") { onWorkspaceCreated(payload); - } else if (eventName == "focusedmon") { + } else if (eventName == "focusedmonv2") { onMonitorFocused(payload); - } else if (eventName == "moveworkspace") { + } else if (eventName == "moveworkspacev2") { onWorkspaceMoved(payload); } else if (eventName == "openwindow") { onWindowOpened(payload); } else if (eventName == "closewindow") { onWindowClosed(payload); - } else if (eventName == "movewindow") { + } else if (eventName == "movewindowv2") { onWindowMoved(payload); } else if (eventName == "urgent") { setUrgentWorkspace(payload); } else if (eventName == "renameworkspace") { onWorkspaceRenamed(payload); - } else if (eventName == "windowtitle") { + } else if (eventName == "windowtitlev2") { onWindowTitleEvent(payload); } else if (eventName == "configreloaded") { onConfigReloaded(); @@ -348,7 +350,11 @@ void Workspaces::onEvent(const std::string &ev) { } void Workspaces::onWorkspaceActivated(std::string const &payload) { - m_activeWorkspaceName = payload; + const auto [workspaceIdStr, workspaceName] = splitDoublePayload(payload); + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (workspaceId.has_value()) { + m_activeWorkspaceId = *workspaceId; + } } void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { @@ -357,39 +363,55 @@ void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { } void Workspaces::onWorkspaceDestroyed(std::string const &payload) { - if (!isDoubleSpecial(payload)) { - m_workspacesToRemove.push_back(payload); + const auto [workspaceId, workspaceName] = splitDoublePayload(payload); + if (!isDoubleSpecial(workspaceName)) { + m_workspacesToRemove.push_back(workspaceId); } } -void Workspaces::onWorkspaceCreated(std::string const &workspaceName, - Json::Value const &clientsData) { - spdlog::debug("Workspace created: {}", workspaceName); - auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); +void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value const &clientsData) { + spdlog::debug("Workspace created: {}", payload); - if (!isWorkspaceIgnored(workspaceName)) { - auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); - for (Json::Value workspaceJson : workspacesJson) { - std::string name = workspaceJson["name"].asString(); - if (name == workspaceName) { - if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && - (showSpecial() || !name.starts_with("special")) && !isDoubleSpecial(workspaceName)) { - for (Json::Value const &rule : workspaceRules) { - if (rule["workspaceString"].asString() == workspaceName) { - workspaceJson["persistent-rule"] = rule["persistent"].asBool(); - break; - } - } + const auto [workspaceIdStr, _] = splitDoublePayload(payload); - m_workspacesToCreate.emplace_back(workspaceJson, clientsData); - break; - } - } else { - extendOrphans(workspaceJson["id"].asInt(), clientsData); + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + + auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); + auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); + + for (Json::Value workspaceJson : workspacesJson) { + const auto currentId = workspaceJson["id"].asInt(); + if (currentId == *workspaceId) { + std::string workspaceName = workspaceJson["name"].asString(); + // This workspace name is more up-to-date than the one in the event payload. + if (isWorkspaceIgnored(workspaceName)) { + spdlog::trace("Not creating workspace because it is ignored: id={} name={}", *workspaceId, + workspaceName); + break; } + + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (showSpecial() || !workspaceName.starts_with("special")) && + !isDoubleSpecial(workspaceName)) { + for (Json::Value const &rule : workspaceRules) { + auto ruleWorkspaceName = rule.isMember("defaultName") + ? rule["defaultName"].asString() + : rule["workspaceString"].asString(); + if (ruleWorkspaceName == workspaceName) { + workspaceJson["persistent-rule"] = rule["persistent"].asBool(); + break; + } + } + + m_workspacesToCreate.emplace_back(workspaceJson, clientsData); + break; + } + } else { + extendOrphans(*workspaceId, clientsData); } - } else { - spdlog::trace("Not creating workspace because it is ignored: {}", workspaceName); } } @@ -397,32 +419,34 @@ void Workspaces::onWorkspaceMoved(std::string const &payload) { spdlog::debug("Workspace moved: {}", payload); // Update active workspace - m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + m_activeWorkspaceId = (m_ipc.getSocket1JsonReply("activeworkspace"))["id"].asInt(); if (allOutputs()) return; - std::string workspaceName = payload.substr(0, payload.find(',')); - std::string monitorName = payload.substr(payload.find(',') + 1); + const auto [workspaceIdStr, workspaceName, monitorName] = splitTriplePayload(payload); + + const auto subPayload = makePayload(workspaceIdStr, workspaceName); if (m_bar.output->name == monitorName) { - Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); - onWorkspaceCreated(workspaceName, clientsData); + Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); + onWorkspaceCreated(subPayload, clientsData); } else { - spdlog::debug("Removing workspace because it was moved to another monitor: {}"); - onWorkspaceDestroyed(workspaceName); + spdlog::debug("Removing workspace because it was moved to another monitor: {}", subPayload); + onWorkspaceDestroyed(subPayload); } } void Workspaces::onWorkspaceRenamed(std::string const &payload) { spdlog::debug("Workspace renamed: {}", payload); - std::string workspaceIdStr = payload.substr(0, payload.find(',')); - int workspaceId = workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr); - std::string newName = payload.substr(payload.find(',') + 1); + const auto [workspaceIdStr, newName] = splitDoublePayload(payload); + + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + for (auto &workspace : m_workspaces) { - if (workspace->id() == workspaceId) { - if (workspace->name() == m_activeWorkspaceName) { - m_activeWorkspaceName = newName; - } + if (workspace->id() == *workspaceId) { workspace->setName(newName); break; } @@ -432,11 +456,19 @@ void Workspaces::onWorkspaceRenamed(std::string const &payload) { void Workspaces::onMonitorFocused(std::string const &payload) { spdlog::trace("Monitor focused: {}", payload); - m_activeWorkspaceName = payload.substr(payload.find(',') + 1); - for (Json::Value &monitor : gIPC->getSocket1JsonReply("monitors")) { - if (monitor["name"].asString() == payload.substr(0, payload.find(','))) { - auto name = monitor["specialWorkspace"]["name"].asString(); + const auto [monitorName, workspaceIdStr] = splitDoublePayload(payload); + + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + + m_activeWorkspaceId = *workspaceId; + + for (Json::Value &monitor : m_ipc.getSocket1JsonReply("monitors")) { + if (monitor["name"].asString() == monitorName) { + const auto name = monitor["specialWorkspace"]["name"].asString(); m_activeSpecialWorkspaceName = !name.starts_with("special:") ? name : name.substr(8); } } @@ -475,11 +507,7 @@ void Workspaces::onWindowClosed(std::string const &addr) { void Workspaces::onWindowMoved(std::string const &payload) { spdlog::trace("Window moved: {}", payload); updateWindowCount(); - size_t lastCommaIdx = 0; - size_t nextCommaIdx = payload.find(','); - std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); - - std::string workspaceName = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); + auto [windowAddress, _, workspaceName] = splitTriplePayload(payload); std::string windowRepr; @@ -487,7 +515,7 @@ void Workspaces::onWindowMoved(std::string const &payload) { // and exit for (auto &window : m_windowsToCreate) { if (window.getAddress() == windowAddress) { - window.moveToWorksace(workspaceName); + window.moveToWorkspace(workspaceName); return; } } @@ -515,13 +543,15 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { spdlog::trace("Window title changed: {}", payload); std::optional> inserter; + const auto [windowAddress, _] = splitDoublePayload(payload); + // If the window was an orphan, rename it at the orphan's vector - if (m_orphanWindowMap.contains(payload)) { + if (m_orphanWindowMap.contains(windowAddress)) { inserter = [this](WindowCreationPayload wcp) { this->registerOrphanWindow(std::move(wcp)); }; } else { - auto windowWorkspace = - std::find_if(m_workspaces.begin(), m_workspaces.end(), - [payload](auto &workspace) { return workspace->containsWindow(payload); }); + auto windowWorkspace = std::ranges::find_if(m_workspaces, [windowAddress](auto &workspace) { + return workspace->containsWindow(windowAddress); + }); // If the window exists on a workspace, rename it at the workspace's window // map @@ -530,9 +560,9 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { (*windowWorkspace)->insertWindow(std::move(wcp)); }; } else { - auto queuedWindow = std::find_if( - m_windowsToCreate.begin(), m_windowsToCreate.end(), - [payload](auto &windowPayload) { return windowPayload.getAddress() == payload; }); + auto queuedWindow = std::ranges::find_if(m_windowsToCreate, [payload](auto &windowPayload) { + return windowPayload.getAddress() == payload; + }); // If the window was queued, rename it in the queue if (queuedWindow != m_windowsToCreate.end()) { @@ -542,15 +572,14 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { } if (inserter.has_value()) { - Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); + Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); std::string jsonWindowAddress = fmt::format("0x{}", payload); - auto client = - std::find_if(clientsData.begin(), clientsData.end(), [jsonWindowAddress](auto &client) { - return client["address"].asString() == jsonWindowAddress; - }); + auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) { + return client["address"].asString() == jsonWindowAddress; + }); - if (!client->empty()) { + if (client != clientsData.end() && !client->empty()) { (*inserter)({*client}); } } @@ -573,6 +602,7 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateBoolConfig(config, "all-outputs", m_allOutputs); populateBoolConfig(config, "show-special", m_showSpecial); populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); + populateBoolConfig(config, "persistent-only", m_persistentOnly); populateBoolConfig(config, "active-only", m_activeOnly); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); @@ -590,8 +620,8 @@ auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { m_iconsMap.emplace("", ""); } -auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, - bool &member) -> void { +auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, bool &member) + -> void { const auto &configValue = config[key]; if (configValue.isBool()) { member = configValue.asBool(); @@ -660,40 +690,58 @@ void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payloa } auto Workspaces::registerIpc() -> void { - gIPC->registerForIPC("workspace", this); - gIPC->registerForIPC("activespecial", this); - gIPC->registerForIPC("createworkspace", this); - gIPC->registerForIPC("destroyworkspace", this); - gIPC->registerForIPC("focusedmon", this); - gIPC->registerForIPC("moveworkspace", this); - gIPC->registerForIPC("renameworkspace", this); - gIPC->registerForIPC("openwindow", this); - gIPC->registerForIPC("closewindow", this); - gIPC->registerForIPC("movewindow", this); - gIPC->registerForIPC("urgent", this); - gIPC->registerForIPC("configreloaded", this); + m_ipc.registerForIPC("workspacev2", this); + m_ipc.registerForIPC("activespecial", this); + m_ipc.registerForIPC("createworkspacev2", this); + m_ipc.registerForIPC("destroyworkspacev2", this); + m_ipc.registerForIPC("focusedmonv2", this); + m_ipc.registerForIPC("moveworkspacev2", this); + m_ipc.registerForIPC("renameworkspace", this); + m_ipc.registerForIPC("openwindow", this); + m_ipc.registerForIPC("closewindow", this); + m_ipc.registerForIPC("movewindowv2", this); + m_ipc.registerForIPC("urgent", this); + m_ipc.registerForIPC("configreloaded", this); if (windowRewriteConfigUsesTitle()) { spdlog::info( - "Registering for Hyprland's 'windowtitle' events because a user-defined window " + "Registering for Hyprland's 'windowtitlev2' events because a user-defined window " "rewrite rule uses the 'title' field."); - gIPC->registerForIPC("windowtitle", this); + m_ipc.registerForIPC("windowtitlev2", this); } } void Workspaces::removeWorkspacesToRemove() { - for (const auto &workspaceName : m_workspacesToRemove) { - removeWorkspace(workspaceName); + for (const auto &workspaceString : m_workspacesToRemove) { + removeWorkspace(workspaceString); } m_workspacesToRemove.clear(); } -void Workspaces::removeWorkspace(std::string const &name) { - spdlog::debug("Removing workspace {}", name); - auto workspace = - std::find_if(m_workspaces.begin(), m_workspaces.end(), [&](std::unique_ptr &x) { - return (name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name(); - }); +void Workspaces::removeWorkspace(std::string const &workspaceString) { + spdlog::debug("Removing workspace {}", workspaceString); + + // If this succeeds, we have a workspace ID. + const auto workspaceId = parseWorkspaceId(workspaceString); + + std::string name; + // TODO: At some point we want to support all workspace selectors + // This is just a subset. + // https://wiki.hyprland.org/Configuring/Workspace-Rules/#workspace-selectors + if (workspaceString.starts_with("special:")) { + name = workspaceString.substr(8); + } else if (workspaceString.starts_with("name:")) { + name = workspaceString.substr(5); + } else { + name = workspaceString; + } + + const auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr &x) { + if (workspaceId.has_value()) { + return *workspaceId == x->id(); + } + return name == x->name(); + }); if (workspace == m_workspaces.end()) { // happens when a workspace on another monitor is destroyed @@ -701,7 +749,8 @@ void Workspaces::removeWorkspace(std::string const &name) { } if ((*workspace)->isPersistentConfig()) { - spdlog::trace("Not removing config persistent workspace {}", name); + spdlog::trace("Not removing config persistent workspace id={} name={}", (*workspace)->id(), + (*workspace)->name()); return; } @@ -712,10 +761,10 @@ void Workspaces::removeWorkspace(std::string const &name) { void Workspaces::setCurrentMonitorId() { // get monitor ID from name (used by persistent workspaces) m_monitorId = 0; - auto monitors = gIPC->getSocket1JsonReply("monitors"); - auto currentMonitor = std::find_if( - monitors.begin(), monitors.end(), - [this](const Json::Value &m) { return m["name"].asString() == m_bar.output->name; }); + auto monitors = m_ipc.getSocket1JsonReply("monitors"); + auto currentMonitor = std::ranges::find_if(monitors, [this](const Json::Value &m) { + return m["name"].asString() == m_bar.output->name; + }); if (currentMonitor == monitors.end()) { spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name); } else { @@ -724,63 +773,101 @@ void Workspaces::setCurrentMonitorId() { } } +void Workspaces::sortSpecialCentered() { + std::vector> specialWorkspaces; + std::vector> hiddenWorkspaces; + std::vector> normalWorkspaces; + + for (auto &workspace : m_workspaces) { + if (workspace->isSpecial()) { + specialWorkspaces.push_back(std::move(workspace)); + } else { + if (workspace->button().is_visible()) { + normalWorkspaces.push_back(std::move(workspace)); + } else { + hiddenWorkspaces.push_back(std::move(workspace)); + } + } + } + m_workspaces.clear(); + + size_t center = normalWorkspaces.size() / 2; + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(normalWorkspaces.begin()), + std::make_move_iterator(normalWorkspaces.begin() + center)); + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(specialWorkspaces.begin()), + std::make_move_iterator(specialWorkspaces.end())); + + m_workspaces.insert(m_workspaces.end(), + std::make_move_iterator(normalWorkspaces.begin() + center), + std::make_move_iterator(normalWorkspaces.end())); + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(hiddenWorkspaces.begin()), + std::make_move_iterator(hiddenWorkspaces.end())); +} + void Workspaces::sortWorkspaces() { - std::sort(m_workspaces.begin(), m_workspaces.end(), - [&](std::unique_ptr &a, std::unique_ptr &b) { - // Helper comparisons - auto isIdLess = a->id() < b->id(); - auto isNameLess = a->name() < b->name(); + std::ranges::sort( // + m_workspaces, [&](std::unique_ptr &a, std::unique_ptr &b) { + // Helper comparisons + auto isIdLess = a->id() < b->id(); + auto isNameLess = a->name() < b->name(); - switch (m_sortBy) { - case SortMethod::ID: - return isIdLess; - case SortMethod::NAME: - return isNameLess; - case SortMethod::NUMBER: - try { - return std::stoi(a->name()) < std::stoi(b->name()); - } catch (const std::invalid_argument &) { - // Handle the exception if necessary. - break; - } - case SortMethod::DEFAULT: - default: - // Handle the default case here. - // normal -> named persistent -> named -> special -> named special + switch (m_sortBy) { + case SortMethod::ID: + return isIdLess; + case SortMethod::NAME: + return isNameLess; + case SortMethod::NUMBER: + try { + return std::stoi(a->name()) < std::stoi(b->name()); + } catch (const std::invalid_argument &) { + // Handle the exception if necessary. + break; + } + case SortMethod::DEFAULT: + default: + // Handle the default case here. + // normal -> named persistent -> named -> special -> named special - // both normal (includes numbered persistent) => sort by ID - if (a->id() > 0 && b->id() > 0) { - return isIdLess; - } + // both normal (includes numbered persistent) => sort by ID + if (a->id() > 0 && b->id() > 0) { + return isIdLess; + } - // one normal, one special => normal first - if ((a->isSpecial()) ^ (b->isSpecial())) { - return b->isSpecial(); - } + // one normal, one special => normal first + if ((a->isSpecial()) ^ (b->isSpecial())) { + return b->isSpecial(); + } - // only one normal, one named - if ((a->id() > 0) ^ (b->id() > 0)) { - return a->id() > 0; - } + // only one normal, one named + if ((a->id() > 0) ^ (b->id() > 0)) { + return a->id() > 0; + } - // both special - if (a->isSpecial() && b->isSpecial()) { - // if one is -99 => put it last - if (a->id() == -99 || b->id() == -99) { - return b->id() == -99; - } - // both are 0 (not yet named persistents) / named specials (-98 <= ID <= -1) - return isNameLess; - } - - // sort non-special named workspaces by name (ID <= -1377) - return isNameLess; - break; + // both special + if (a->isSpecial() && b->isSpecial()) { + // if one is -99 => put it last + if (a->id() == -99 || b->id() == -99) { + return b->id() == -99; } + // both are 0 (not yet named persistents) / named specials + // (-98 <= ID <= -1) + return isNameLess; + } - // Return a default value if none of the cases match. - return isNameLess; // You can adjust this to your specific needs. - }); + // sort non-special named workspaces by name (ID <= -1377) + return isNameLess; + break; + } + + // Return a default value if none of the cases match. + return isNameLess; // You can adjust this to your specific needs. + }); + if (m_sortBy == SortMethod::SPECIAL_CENTERED) { + this->sortSpecialCentered(); + } for (size_t i = 0; i < m_workspaces.size(); ++i) { m_box.reorder_child(m_workspaces[i]->button(), i); @@ -788,7 +875,7 @@ void Workspaces::sortWorkspaces() { } void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { - const Json::Value clientsJson = gIPC->getSocket1JsonReply("clients"); + const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients"); int workspaceId = -1; for (Json::Value clientJson : clientsJson) { @@ -798,9 +885,9 @@ void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { } } - auto workspace = - std::find_if(m_workspaces.begin(), m_workspaces.end(), - [workspaceId](std::unique_ptr &x) { return x->id() == workspaceId; }); + auto workspace = std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr &x) { + return x->id() == workspaceId; + }); if (workspace != m_workspaces.end()) { workspace->get()->setUrgent(); } @@ -812,13 +899,12 @@ auto Workspaces::update() -> void { } void Workspaces::updateWindowCount() { - const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); for (auto &workspace : m_workspaces) { - auto workspaceJson = - std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { - return x["name"].asString() == workspace->name() || - (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); - }); + auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) { + return x["name"].asString() == workspace->name() || + (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + }); uint32_t count = 0; if (workspaceJson != workspacesJson.end()) { try { @@ -858,26 +944,34 @@ bool Workspaces::updateWindowsToCreate() { } void Workspaces::updateWorkspaceStates() { - const std::vector visibleWorkspaces = getVisibleWorkspaces(); - auto updatedWorkspaces = gIPC->getSocket1JsonReply("workspaces"); + const std::vector visibleWorkspaces = getVisibleWorkspaces(); + auto updatedWorkspaces = m_ipc.getSocket1JsonReply("workspaces"); + + auto currentWorkspace = m_ipc.getSocket1JsonReply("activeworkspace"); + std::string currentWorkspaceName = + currentWorkspace.isMember("name") ? currentWorkspace["name"].asString() : ""; + for (auto &workspace : m_workspaces) { - workspace->setActive(workspace->name() == m_activeWorkspaceName || - workspace->name() == m_activeSpecialWorkspaceName); - if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { + bool isActiveByName = + !currentWorkspaceName.empty() && workspace->name() == currentWorkspaceName; + + workspace->setActive( + workspace->id() == m_activeWorkspaceId || isActiveByName || + (workspace->isSpecial() && workspace->name() == m_activeSpecialWorkspaceName)); + if (workspace->isActive() && workspace->isUrgent()) { workspace->setUrgent(false); } - workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), - workspace->name()) != visibleWorkspaces.end()); + workspace->setVisible(std::ranges::find(visibleWorkspaces, workspace->id()) != + visibleWorkspaces.end()); std::string &workspaceIcon = m_iconsMap[""]; if (m_withIcon) { workspaceIcon = workspace->selectIcon(m_iconsMap); } - auto updatedWorkspace = std::find_if( - updatedWorkspaces.begin(), updatedWorkspaces.end(), [&workspace](const auto &w) { - auto wNameRaw = w["name"].asString(); - auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; - return wName == workspace->name(); - }); + auto updatedWorkspace = std::ranges::find_if(updatedWorkspaces, [&workspace](const auto &w) { + auto wNameRaw = w["name"].asString(); + auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; + return wName == workspace->name(); + }); if (updatedWorkspace != updatedWorkspaces.end()) { workspace->setOutput((*updatedWorkspace)["monitor"].asString()); } @@ -905,4 +999,39 @@ int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) { return 0; } +template +std::string Workspaces::makePayload(Args const &...args) { + std::ostringstream result; + bool first = true; + ((result << (first ? "" : ",") << args, first = false), ...); + return result.str(); +} + +std::pair Workspaces::splitDoublePayload(std::string const &payload) { + const std::string part1 = payload.substr(0, payload.find(',')); + const std::string part2 = payload.substr(part1.size() + 1); + return {part1, part2}; +} + +std::tuple Workspaces::splitTriplePayload( + std::string const &payload) { + const size_t firstComma = payload.find(','); + const size_t secondComma = payload.find(',', firstComma + 1); + + const std::string part1 = payload.substr(0, firstComma); + const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1)); + const std::string part3 = payload.substr(secondComma + 1); + + return {part1, part2, part3}; +} + +std::optional Workspaces::parseWorkspaceId(std::string const &workspaceIdStr) { + try { + return workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr); + } catch (std::exception const &e) { + spdlog::error("Failed to parse workspace ID: {}", e.what()); + return std::nullopt; + } +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 544d7814..18600cd2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -60,6 +60,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); @@ -72,6 +73,7 @@ auto waybar::modules::Memory::update() -> void { fmt::runtime(tooltip_format), used_ram_percentage, fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index ed383b0c..2b345fc5 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -14,7 +14,6 @@ extern "C" { #include #include - namespace waybar::modules::mpris { const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}"; @@ -425,9 +424,11 @@ auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlaye auto* mpris = static_cast(data); if (!mpris) return; - spdlog::debug("mpris: player-vanished callback: {}", player_name->name); + spdlog::debug("mpris: name-vanished callback: {}", player_name->name); - if (std::string(player_name->name) == mpris->player_) { + if (mpris->player_ == "playerctld") { + mpris->dp.emit(); + } else if (mpris->player_ == player_name->name) { mpris->player = nullptr; mpris->event_box_.set_visible(false); mpris->dp.emit(); @@ -498,7 +499,10 @@ auto Mpris::getPlayerInfo() -> std::optional { // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 players = g_list_first(players); - if (players) player_name = static_cast(players->data)->name; + if (players) + player_name = static_cast(players->data)->name; + else + return std::nullopt; // no players found, hide the widget } if (std::any_of(ignored_players_.begin(), ignored_players_.end(), @@ -584,38 +588,45 @@ errorexit: } bool Mpris::handleToggle(GdkEventButton* const& e) { + if (!e || e->type != GdkEventType::GDK_BUTTON_PRESS) { + return false; + } + + auto info = getPlayerInfo(); + if (!info) return false; + + struct ButtonAction { + guint button; + const char* config_key; + std::function builtin_action; + }; + GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } }); - auto info = getPlayerInfo(); - if (!info) return false; + // Command pattern: encapsulate each button's action + const ButtonAction actions[] = { + {1, "on-click", [&]() { playerctl_player_play_pause(player, &error); }}, + {2, "on-click-middle", [&]() { playerctl_player_previous(player, &error); }}, + {3, "on-click-right", [&]() { playerctl_player_next(player, &error); }}, + {8, "on-click-backward", [&]() { playerctl_player_previous(player, &error); }}, + {9, "on-click-forward", [&]() { playerctl_player_next(player, &error); }}, + }; - if (e->type == GdkEventType::GDK_BUTTON_PRESS) { - switch (e->button) { - case 1: // left-click - if (config_["on-click"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_play_pause(player, &error); - break; - case 2: // middle-click - if (config_["on-click-middle"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_previous(player, &error); - break; - case 3: // right-click - if (config_["on-click-right"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_next(player, &error); - break; + for (const auto& action : actions) { + if (e->button == action.button) { + if (config_[action.config_key].isString()) { + return ALabel::handleToggle(e); + } + action.builtin_action(); + break; } } + if (error) { spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name, error->message); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 0bbea631..cc096f72 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -80,7 +80,7 @@ waybar::modules::Network::readBandwidthUsage() { waybar::modules::Network::Network(const std::string &id, const Json::Value &config) : ALabel(config, "network", id, DEFAULT_FORMAT, 60), ifid_(-1), - family_(config["family"] == "ipv6" ? AF_INET6 : AF_INET), + addr_pref_(IPV4), efd_(-1), ev_fd_(-1), want_route_dump_(false), @@ -89,6 +89,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf dump_in_progress_(false), is_p2p_(false), cidr_(0), + cidr6_(0), signal_strength_dbm_(0), signal_strength_(0), #ifdef WANT_RFKILL @@ -102,6 +103,12 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf // the module start with no text, but the event_box_ is shown. label_.set_markup(""); + if (config_["family"] == "ipv6") { + addr_pref_ = IPV6; + } else if (config["family"] == "ipv4_6") { + addr_pref_ = IPV4_6; + } + auto bandwidth = readBandwidthUsage(); if (bandwidth.has_value()) { bandwidth_down_total_ = (*bandwidth).first; @@ -141,12 +148,7 @@ waybar::modules::Network::~Network() { close(efd_); } if (ev_sock_ != nullptr) { - nl_socket_drop_membership(ev_sock_, RTNLGRP_LINK); - if (family_ == AF_INET) { - nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV4_IFADDR); - } else { - nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV6_IFADDR); - } + nl_socket_drop_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR); nl_close(ev_sock_); nl_socket_free(ev_sock_); } @@ -161,7 +163,7 @@ void waybar::modules::Network::createEventSocket() { nl_socket_disable_seq_check(ev_sock_); nl_socket_modify_cb(ev_sock_, NL_CB_VALID, NL_CB_CUSTOM, handleEvents, this); nl_socket_modify_cb(ev_sock_, NL_CB_FINISH, NL_CB_CUSTOM, handleEventsDone, this); - auto groups = RTMGRP_LINK | (family_ == AF_INET ? RTMGRP_IPV4_IFADDR : RTMGRP_IPV6_IFADDR); + auto groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; nl_join_groups(ev_sock_, groups); // Deprecated if (nl_connect(ev_sock_, NETLINK_ROUTE) != 0) { throw std::runtime_error("Can't connect network socket"); @@ -169,18 +171,9 @@ void waybar::modules::Network::createEventSocket() { if (nl_socket_set_nonblocking(ev_sock_)) { throw std::runtime_error("Can't set non-blocking on network socket"); } - nl_socket_add_membership(ev_sock_, RTNLGRP_LINK); - if (family_ == AF_INET) { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_IFADDR); - } else { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_IFADDR); - } + nl_socket_add_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR, 0); if (!config_["interface"].isString()) { - if (family_ == AF_INET) { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_ROUTE); - } else { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_ROUTE); - } + nl_socket_add_memberships(ev_sock_, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV6_ROUTE, 0); } efd_ = epoll_create1(EPOLL_CLOEXEC); @@ -230,8 +223,8 @@ void waybar::modules::Network::worker() { std::lock_guard lock(mutex_); if (ifid_ > 0) { getInfo(); - dp.emit(); } + dp.emit(); } thread_timer_.sleep_for(interval_); }; @@ -278,14 +271,12 @@ void waybar::modules::Network::worker() { } const std::string waybar::modules::Network::getNetworkState() const { - if (ifid_ == -1) { #ifdef WANT_RFKILL - if (rfkill_.getState()) return "disabled"; + if (rfkill_.getState() && ifid_ == -1) return "disabled"; #endif - return "disconnected"; - } + if (ifid_ == -1) return "disconnected"; if (!carrier_) return "disconnected"; - if (ipaddr_.empty()) return "linked"; + if (ipaddr_.empty() && ipaddr6_.empty()) return "linked"; if (essid_.empty()) return "ethernet"; return "wifi"; } @@ -331,12 +322,24 @@ auto waybar::modules::Network::update() -> void { } getState(signal_strength_); + std::string final_ipaddr_; + if (addr_pref_ == ip_addr_pref::IPV4) { + final_ipaddr_ = ipaddr_; + } else if (addr_pref_ == ip_addr_pref::IPV6) { + final_ipaddr_ = ipaddr6_; + } else if (addr_pref_ == ip_addr_pref::IPV4_6) { + final_ipaddr_ = ipaddr_; + final_ipaddr_ += '\n'; + final_ipaddr_ += ipaddr6_; + } + auto text = fmt::format( fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), - fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), + fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), + fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), + fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), @@ -367,8 +370,9 @@ auto waybar::modules::Network::update() -> void { fmt::runtime(tooltip_format), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), - fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), + fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), + fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), + fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), @@ -409,10 +413,13 @@ void waybar::modules::Network::clearIface() { essid_.clear(); bssid_.clear(); ipaddr_.clear(); + ipaddr6_.clear(); gwaddr_.clear(); netmask_.clear(); + netmask6_.clear(); carrier_ = false; cidr_ = 0; + cidr6_ = 0; signal_strength_dbm_ = 0; signal_strength_ = 0; signal_strength_app_.clear(); @@ -531,16 +538,11 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { return NL_OK; } - if (ifa->ifa_family != net->family_) { - return NL_OK; - } - // We ignore address mark as scope for the link or host, // which should leave scope global addresses. if (ifa->ifa_scope >= RT_SCOPE_LINK) { return NL_OK; } - for (; RTA_OK(ifa_rta, attrlen); ifa_rta = RTA_NEXT(ifa_rta, attrlen)) { switch (ifa_rta->rta_type) { case IFA_ADDRESS: @@ -548,8 +550,20 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { case IFA_LOCAL: char ipaddr[INET6_ADDRSTRLEN]; if (!is_del_event) { - net->ipaddr_ = inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr_ = ifa->ifa_prefixlen; + if ((net->addr_pref_ == ip_addr_pref::IPV4 || + net->addr_pref_ == ip_addr_pref::IPV4_6) && + net->cidr_ == 0 && ifa->ifa_family == AF_INET) { + net->ipaddr_ = + inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); + net->cidr_ = ifa->ifa_prefixlen; + } else if ((net->addr_pref_ == ip_addr_pref::IPV6 || + net->addr_pref_ == ip_addr_pref::IPV4_6) && + net->cidr6_ == 0 && ifa->ifa_family == AF_INET6) { + net->ipaddr6_ = + inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); + net->cidr6_ = ifa->ifa_prefixlen; + } + switch (ifa->ifa_family) { case AF_INET: { struct in_addr netmask; @@ -557,21 +571,24 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr)); } case AF_INET6: { - struct in6_addr netmask; + struct in6_addr netmask6; for (int i = 0; i < 16; i++) { int v = (i + 1) * 8 - ifa->ifa_prefixlen; if (v < 0) v = 0; if (v > 8) v = 8; - netmask.s6_addr[i] = ~0 << v; + netmask6.s6_addr[i] = ~0 << v; } - net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr)); + net->netmask6_ = inet_ntop(ifa->ifa_family, &netmask6, ipaddr, sizeof(ipaddr)); } } spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_); } else { net->ipaddr_.clear(); + net->ipaddr6_.clear(); net->cidr_ = 0; + net->cidr6_ = 0; net->netmask_.clear(); + net->netmask6_.clear(); spdlog::debug("network: {} addr deleted {}/{}", net->ifname_, inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)), ifa->ifa_prefixlen); @@ -591,6 +608,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { // to find the interface used to reach the outside world struct rtmsg *rtm = static_cast(NLMSG_DATA(nh)); + int family = rtm->rtm_family; ssize_t attrlen = RTM_PAYLOAD(nh); struct rtattr *attr = RTM_RTA(rtm); bool has_gateway = false; @@ -618,14 +636,14 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { * If someone ever needs to figure out the gateway address as well, * it's here as the attribute payload. */ - inet_ntop(net->family_, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); + inet_ntop(family, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); has_gateway = true; break; case RTA_DST: { /* The destination address. * Should be either missing, or maybe all 0s. Accept both. */ - const uint32_t nr_zeroes = (net->family_ == AF_INET) ? 4 : 16; + const uint32_t nr_zeroes = (family == AF_INET) ? 4 : 16; unsigned char c = 0; size_t dstlen = RTA_PAYLOAD(attr); if (dstlen != nr_zeroes) { @@ -717,7 +735,6 @@ void waybar::modules::Network::askForStateDump(void) { }; if (want_route_dump_) { - rt_hdr.rtgen_family = family_; nl_send_simple(ev_sock_, RTM_GETROUTE, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr)); want_route_dump_ = false; dump_in_progress_ = true; @@ -728,7 +745,6 @@ void waybar::modules::Network::askForStateDump(void) { dump_in_progress_ = true; } else if (want_addr_dump_) { - rt_hdr.rtgen_family = family_; nl_send_simple(ev_sock_, RTM_GETADDR, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr)); want_addr_dump_ = false; dump_in_progress_ = true; diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 383bf113..fa4dc287 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -147,6 +147,17 @@ void IPC::parseIPC(const std::string &line) { } else { spdlog::error("Active window changed on unknown workspace"); } + } else if (const auto &payload = ev["WorkspaceUrgencyChanged"]) { + const auto id = payload["id"].asUInt64(); + const auto urgent = payload["urgent"].asBool(); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [id](const auto &ws) { return ws["id"].asUInt64() == id; }); + if (it != workspaces_.end()) { + auto &ws = *it; + ws["is_urgent"] = urgent; + } else { + spdlog::error("Urgency changed for unknown workspace"); + } } else if (const auto &payload = ev["KeyboardLayoutsChanged"]) { const auto &layouts = payload["keyboard_layouts"]; const auto &names = layouts["names"]; diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 1e4d6d10..3b55ff24 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -9,7 +9,7 @@ namespace waybar::modules::niri { Language::Language(const std::string &id, const Bar &bar, const Json::Value &config) - : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "language", id, "{}", 0, false), bar_(bar) { label_.hide(); if (!gIPC) gIPC = std::make_unique(); diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index d2fcad5d..7dfc0b35 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -20,6 +20,7 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value gIPC->registerForIPC("WorkspacesChanged", this); gIPC->registerForIPC("WorkspaceActivated", this); gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); + gIPC->registerForIPC("WorkspaceUrgencyChanged", this); dp.emit(); } @@ -67,6 +68,11 @@ void Workspaces::doUpdate() { else style_context->remove_class("active"); + if (ws["is_urgent"].asBool()) + style_context->add_class("urgent"); + else + style_context->remove_class("urgent"); + if (ws["output"]) { if (ws["output"].asString() == bar_.output->name) style_context->add_class("current_output"); @@ -166,6 +172,10 @@ std::string Workspaces::getIcon(const std::string &value, const Json::Value &ws) const auto &icons = config_["format-icons"]; if (!icons) return value; + if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); + + if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); + if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index 3ae3ae83..abad763d 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -29,14 +29,14 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu // method on the proxy to see whether or not something's responding // on the other side. - // NOTE: the DBus adresses are under migration. They should be + // NOTE: the DBus addresses are under migration. They should be // changed to org.freedesktop.UPower.PowerProfiles at some point. // // See // https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20 // // The old name is still announced for now. Let's rather use the old - // adresses for compatibility sake. + // addresses for compatibility sake. // // Revisit this in 2026, systems should be updated by then. diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 97996c33..904c8fd9 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -15,13 +15,14 @@ using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; -Privacy::Privacy(const std::string& id, const Json::Value& config, const std::string& pos) +Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientation orientation, + const std::string& pos) : AModule(config, "privacy", id), nodes_screenshare(), nodes_audio_in(), nodes_audio_out(), visibility_conn(), - box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_(orientation, 0) { box_.set_name(name_); event_box_.add(box_); @@ -67,12 +68,30 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st auto iter = typeMap.find(type); if (iter != typeMap.end()) { auto& [nodePtr, nodeType] = iter->second; - auto* item = Gtk::make_managed(module, nodeType, nodePtr, pos, iconSize, - transition_duration); + auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, + iconSize, transition_duration); box_.add(*item); } } + for (const auto& ignore_item : config_["ignore"]) { + if (!ignore_item.isObject() || !ignore_item["type"].isString() || + !ignore_item["name"].isString()) + continue; + const std::string type = ignore_item["type"].asString(); + const std::string name = ignore_item["name"].asString(); + + auto iter = typeMap.find(type); + if (iter != typeMap.end()) { + auto& [_, nodeType] = iter->second; + ignore.emplace(nodeType, std::move(name)); + } + } + + if (config_["ignore-monitor"].isBool()) { + ignore_monitor = config_["ignore-monitor"].asBool(); + } + backend = util::PipewireBackend::PipewireBackend::getInstance(); backend->privacy_nodes_changed_signal_event.connect( sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); @@ -87,6 +106,11 @@ void Privacy::onPrivacyNodesChanged() { nodes_screenshare.clear(); for (auto& node : backend->privacy_nodes) { + if (ignore_monitor && node.second->is_monitor) continue; + + auto iter = ignore.find(std::pair(node.second->type, node.second->node_name)); + if (iter != ignore.end()) continue; + switch (node.second->state) { case PW_NODE_STATE_RUNNING: switch (node.second->type) { diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index a38b95a4..6424da9e 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -1,5 +1,7 @@ #include "modules/privacy/privacy_item.hpp" +#include + #include #include "glibmm/main.h" @@ -11,8 +13,9 @@ namespace waybar::modules::privacy { PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, - std::list *nodes_, const std::string &pos, - const uint icon_size, const uint transition_duration) + std::list *nodes_, Gtk::Orientation orientation, + const std::string &pos, const uint icon_size, + const uint transition_duration) : Gtk::Revealer(), privacy_type(privacy_type_), nodes(nodes_), @@ -40,16 +43,24 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac // Set the reveal transition to not look weird when sliding in if (pos == "modules-left") { - set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT); + set_transition_type(orientation == Gtk::ORIENTATION_HORIZONTAL + ? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT + : Gtk::REVEALER_TRANSITION_TYPE_SLIDE_DOWN); } else if (pos == "modules-center") { set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE); } else if (pos == "modules-right") { - set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT); + set_transition_type(orientation == Gtk::ORIENTATION_HORIZONTAL + ? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT + : Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP); } set_transition_duration(transition_duration); box_.set_name("privacy-item"); - box_.add(icon_); + + // We use `set_center_widget` instead of `add` to make sure the icon is + // centered even if the orientation is vertical + box_.set_center_widget(icon_); + icon_.set_pixel_size(icon_size); add(box_); @@ -87,20 +98,22 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac void PrivacyItem::update_tooltip() { // Removes all old nodes for (auto *child : tooltip_window.get_children()) { + tooltip_window.remove(*child); + // despite the remove, still needs a delete to prevent memory leak. Speculating that this might + // work differently in GTK4. delete child; } - for (auto *node : *nodes) { - Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + auto *box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, 4); // Set device icon - Gtk::Image *node_icon = new Gtk::Image(); + auto *node_icon = Gtk::make_managed(); node_icon->set_pixel_size(tooltipIconSize); node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); box->add(*node_icon); // Set model - auto *nodeName = new Gtk::Label(node->getName()); + auto *nodeName = Gtk::make_managed(node->getName()); box->add(*nodeName); tooltip_window.add(*box); diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 255ca571..ceed20dd 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -132,7 +132,7 @@ auto waybar::modules::Pulseaudio::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 9e7cd5aa..359e5a23 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -189,10 +189,20 @@ bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { } void Tags::handle_focused_tags(uint32_t tags) { + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool occupied = buttons_[i].get_style_context()->has_class("occupied"); + bool urgent = buttons_[i].get_style_context()->has_class("urgent"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("focused"); } else { + if (hide_vacant && !(occupied || urgent)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("focused"); } } @@ -205,20 +215,40 @@ void Tags::handle_view_tags(struct wl_array *view_tags) { for (; view_tag < end; ++view_tag) { tags |= *view_tag; } + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool focused = buttons_[i].get_style_context()->has_class("focused"); + bool urgent = buttons_[i].get_style_context()->has_class("urgent"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("occupied"); } else { + if (hide_vacant && !(focused || urgent)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("occupied"); } } } void Tags::handle_urgent_tags(uint32_t tags) { + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool occupied = buttons_[i].get_style_context()->has_class("occupied"); + bool focused = buttons_[i].get_style_context()->has_class("focused"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("urgent"); } else { + if (hide_vacant && !(occupied || focused)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("urgent"); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 6c4ec8c0..407d7e72 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -5,10 +5,12 @@ #include #include +#include #include #include #include "gdk/gdk.h" +#include "modules/sni/icon_manager.hpp" #include "util/format.hpp" #include "util/gtk_icon.hpp" @@ -124,7 +126,8 @@ ToolTip get_variant(const Glib::VariantBase& value) { result.text = get_variant(container.get_child(2)); auto description = get_variant(container.get_child(3)); if (!description.empty()) { - result.text = fmt::format("{}\n{}", result.text, description); + auto escapedDescription = Glib::Markup::escape_text(description); + result.text = fmt::format("{}\n{}", result.text, escapedDescription); } return result; } @@ -137,6 +140,7 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { category = get_variant(value); } else if (name == "Id") { id = get_variant(value); + setCustomIcon(id); } else if (name == "Title") { title = get_variant(value); if (tooltip.text.empty()) { @@ -198,6 +202,19 @@ void Item::setStatus(const Glib::ustring& value) { style->add_class(lower); } +void Item::setCustomIcon(const std::string& id) { + std::string custom_icon = IconManager::instance().getIconForApp(id); + if (!custom_icon.empty()) { + if (std::filesystem::exists(custom_icon)) { + Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); + icon_name = ""; // icon_name has priority over pixmap + icon_pixmap = custom_pixbuf; + } else { // if file doesn't exist it's most likely an icon_name + icon_name = custom_icon; + } + } +} + void Item::getUpdatedProperties() { auto params = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(SNI_INTERFACE_NAME)}); @@ -371,14 +388,17 @@ Glib::RefPtr Item::getIconPixbuf() { Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { icon_theme->rescan_if_needed(); - if (!icon_theme_path.empty() && - icon_theme->lookup_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) { - return icon_theme->load_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + if (!icon_theme_path.empty()) { + auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + if (icon_info) { + bool is_sym = false; + return icon_info.load_symbolic(event_box.get_style_context(), is_sym); + } } return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE, + event_box.get_style_context()); } double Item::getScaledIconSize() { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index a2c56808..f657c855 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -2,6 +2,8 @@ #include +#include "modules/sni/icon_manager.hpp" + namespace waybar::modules::SNI { Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) @@ -20,6 +22,9 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) box_.set_spacing(config_["spacing"].asUInt()); } nb_hosts_ += 1; + if (config_["icons"].isObject()) { + IconManager::instance().setIconsConfig(config_["icons"]); + } dp.emit(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index a005df17..f4cfa6c3 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -22,10 +22,10 @@ Language::Language(const std::string& id, const Json::Value& config) hide_single_ = config["hide-single-layout"].isBool() && config["hide-single-layout"].asBool(); is_variant_displayed = format_.find("{variant}") != std::string::npos; if (format_.find("{}") != std::string::npos || format_.find("{short}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortName); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortName); } if (format_.find("{shortDescription}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortDescription); } if (config.isMember("tooltip-format")) { tooltip_format_ = config["tooltip-format"].asString(); diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 25e430a7..68655a76 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -41,8 +41,8 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); auto output = payload["output"].isString() ? payload["output"].asString() : ""; - std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_) = - getFocusedNode(payload["nodes"], output); + std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_, + marks_) = getFocusedNode(payload["nodes"], output); updateAppIconName(app_id_, app_class_); dp.emit(); } catch (const std::exception& e) { @@ -96,7 +96,7 @@ auto Window::update() -> void { label_.set_markup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), - fmt::arg("shell", shell_)), + fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { label_.set_tooltip_text(window_); @@ -108,7 +108,7 @@ auto Window::update() -> void { AAppIconLabel::update(); } -void Window::setClass(std::string classname, bool enable) { +void Window::setClass(const std::string& classname, bool enable) { if (enable) { if (!bar_.window.get_style_context()->has_class(classname)) { bar_.window.get_style_context()->add_class(classname); @@ -169,17 +169,31 @@ std::optional> getSingleChildNode( return {getSingleChildNode(child)}; } -std::tuple getWindowInfo(const Json::Value& node) { +std::tuple getWindowInfo( + const Json::Value& node, bool showHidden) { const auto app_id = node["app_id"].isString() ? node["app_id"].asString() : node["window_properties"]["instance"].asString(); const auto app_class = node["window_properties"]["class"].isString() ? node["window_properties"]["class"].asString() : ""; const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; - return {app_id, app_class, shell}; + std::string marks = ""; + if (node["marks"].isArray()) { + for (const auto& m : node["marks"]) { + if (!m.isString() || (!showHidden && m.asString().at(0) == '_')) { + continue; + } + if (!marks.empty()) { + marks += ','; + } + marks += m.asString(); + } + } + return {app_id, app_class, shell, marks}; } -std::tuple +std::tuple gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_, Json::Value& parentWorkspace, const Json::Value& immediateParent) { @@ -207,7 +221,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu "", "", "", - node["layout"].asString()}; + node["layout"].asString(), + ""}; } parentWorkspace = node; } else if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && @@ -215,7 +230,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu // found node spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name, output, node["name"].asString()); - const auto [app_id, app_class, shell] = getWindowInfo(node); + const auto [app_id, app_class, shell, marks] = + getWindowInfo(node, config_["show-hidden-marks"].asBool()); int nb = node.size(); int floating_count = 0; std::string workspace_layout = ""; @@ -232,20 +248,21 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu app_id, app_class, shell, - workspace_layout}; + workspace_layout, + marks}; } // iterate - auto [nb, f, id, name, app_id, app_class, shell, workspace_layout] = + auto [nb, f, id, name, app_id, app_class, shell, workspace_layout, marks] = gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace, node); - auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2] = + auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2, marks2] = gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace, node); // if ((id > 0 || ((id2 < 0 || name2.empty()) && id > -1)) && !name.empty()) { if ((id > 0) || (id2 < 0 && id > -1)) { - return {nb, f, id, name, app_id, app_class, shell, workspace_layout}; + return {nb, f, id, name, app_id, app_class, shell, workspace_layout, marks}; } else if (id2 > 0 && !name2.empty()) { - return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2}; + return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2, marks2}; } } @@ -258,10 +275,12 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu std::string app_id = ""; std::string app_class = ""; std::string workspace_layout = ""; + std::string marks = ""; if (all_leaf_nodes.first == 1) { const auto single_child = getSingleChildNode(immediateParent); if (single_child.has_value()) { - std::tie(app_id, app_class, workspace_layout) = getWindowInfo(single_child.value()); + std::tie(app_id, app_class, workspace_layout, marks) = + getWindowInfo(single_child.value(), config_["show-hidden-marks"].asBool()); } } return {all_leaf_nodes.first, @@ -273,13 +292,15 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu app_id, app_class, workspace_layout, - immediateParent["layout"].asString()}; + immediateParent["layout"].asString(), + marks}; } - return {0, 0, -1, "", "", "", "", ""}; + return {0, 0, -1, "", "", "", "", "", ""}; } -std::tuple +std::tuple Window::getFocusedNode(const Json::Value& nodes, std::string& output) { Json::Value placeholder = Json::Value::null; return gfnWithWorkspace(nodes, output, config_, bar_, placeholder, placeholder); diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 8f273300..7a8ce571 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -57,19 +57,18 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); if (config_["format-window-separator"].isString()) { - m_formatWindowSeperator = config_["format-window-separator"].asString(); + m_formatWindowSeparator = config_["format-window-separator"].asString(); } else { - m_formatWindowSeperator = " "; + m_formatWindowSeparator = " "; } const Json::Value &windowRewrite = config["window-rewrite"]; - - const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; - m_windowRewriteDefault = - windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; - - m_windowRewriteRules = waybar::util::RegexCollection( - windowRewrite, m_windowRewriteDefault, - [](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); + if (windowRewrite.isObject()) { + const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; + std::string windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; + m_windowRewriteRules = waybar::util::RegexCollection( + windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction); + } ipc_.subscribe(R"(["workspace"])"); ipc_.subscribe(R"(["window"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent)); @@ -260,7 +259,9 @@ void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && node["name"].isString()) { std::string title = g_markup_escape_text(node["name"].asString().c_str(), -1); - std::string windowClass = node["app_id"].asString(); + std::string windowClass = node["app_id"].isString() + ? node["app_id"].asString() + : node["window_properties"]["class"].asString(); // Only add window rewrites that can be looked up if (!windowClass.empty()) { @@ -270,7 +271,7 @@ void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { window = fmt::format(fmt::runtime(window), fmt::arg("name", title), fmt::arg("class", windowClass)); windows.append(window); - windows.append(m_formatWindowSeperator); + windows.append(m_formatWindowSeparator); } } for (const Json::Value &child : node["nodes"]) { @@ -339,7 +340,7 @@ auto Workspaces::update() -> void { fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output), fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()), fmt::arg("windows", - windows.substr(0, windows.length() - m_formatWindowSeperator.length())), + windows.substr(0, windows.length() - m_formatWindowSeparator.length())), fmt::arg("output", (*it)["output"].asString())); } if (!config_["disable-markup"].asBool()) { @@ -493,16 +494,34 @@ std::string Workspaces::trimWorkspaceName(std::string name) { return name; } +bool is_focused_recursive(const Json::Value &node) { + // If a workspace has a focused container then get_tree will say + // that the workspace itself isn't focused. Therefore we need to + // check if any of its nodes are focused as well. + // some layouts like tabbed have many nested nodes + // all nested nodes must be checked for focused flag + if (node["focused"].asBool()) { + return true; + } + + for (const auto &child : node["nodes"]) { + if (is_focused_recursive(child)) { + return true; + } + } + + for (const auto &child : node["floating_nodes"]) { + if (is_focused_recursive(child)) { + return true; + } + } + + return false; +} + void Workspaces::onButtonReady(const Json::Value &node, Gtk::Button &button) { if (config_["current-only"].asBool()) { - // If a workspace has a focused container then get_tree will say - // that the workspace itself isn't focused. Therefore we need to - // check if any of its nodes are focused as well. - bool focused = node["focused"].asBool() || - std::any_of(node["nodes"].begin(), node["nodes"].end(), - [](const auto &child) { return child["focused"].asBool(); }); - - if (focused) { + if (is_focused_recursive(node)) { button.show(); } else { button.hide(); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 56e624cf..90f33be7 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -16,6 +16,7 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& update_pending(false), nr_failed_system(0), nr_failed_user(0), + nr_failed(0), last_status() { if (config["hide-on-ok"].isBool()) { hide_on_ok = config["hide-on-ok"].asBool(); @@ -67,11 +68,38 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, } } -void SystemdFailedUnits::updateData() { - update_pending = false; +void SystemdFailedUnits::RequestSystemState() { + auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { + try { + if (!proxy) return "unknown"; + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + if (data && data.is_of_type(Glib::VariantType("(v)"))) { + Glib::VariantBase variant; + g_variant_get(data.gobj_copy(), "(v)", &variant); + if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { + return g_variant_get_string(variant.gobj_copy(), NULL); + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} state: {}", kind, e.what().c_str()); + } + return "unknown"; + }; + system_state = load("systemwide", system_proxy); + user_state = load("user", user_proxy); + if (system_state == "running" && user_state == "running") + overall_state = "ok"; + else + overall_state = "degraded"; +} + +void SystemdFailedUnits::RequestFailedUnits() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> uint32_t { try { + if (!proxy) return 0; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits")); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); @@ -79,9 +107,7 @@ void SystemdFailedUnits::updateData() { Glib::VariantBase variant; g_variant_get(data.gobj_copy(), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { - uint32_t value = 0; - g_variant_get(variant.gobj_copy(), "u", &value); - return value; + return g_variant_get_uint32(variant.gobj_copy()); } } } catch (Glib::Error& e) { @@ -90,40 +116,46 @@ void SystemdFailedUnits::updateData() { return 0; }; - if (system_proxy) { - nr_failed_system = load("systemwide", system_proxy); - } - if (user_proxy) { - nr_failed_user = load("user", user_proxy); - } + nr_failed_system = load("systemwide", system_proxy); + nr_failed_user = load("user", user_proxy); + nr_failed = nr_failed_system + nr_failed_user; +} + +void SystemdFailedUnits::updateData() { + update_pending = false; + + RequestSystemState(); + if (overall_state == "degraded") RequestFailedUnits(); + dp.emit(); } auto SystemdFailedUnits::update() -> void { - uint32_t nr_failed = nr_failed_system + nr_failed_user; + if (last_status == overall_state) return; // Hide if needed. - if (nr_failed == 0 && hide_on_ok) { + if (overall_state == "ok" && hide_on_ok) { event_box_.set_visible(false); return; } - if (!event_box_.get_visible()) { - event_box_.set_visible(true); - } + + event_box_.set_visible(true); // Set state class. - const std::string status = nr_failed == 0 ? "ok" : "degraded"; if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { label_.get_style_context()->remove_class(last_status); } - if (!label_.get_style_context()->has_class(status)) { - label_.get_style_context()->add_class(status); + if (!label_.get_style_context()->has_class(overall_state)) { + label_.get_style_context()->add_class(overall_state); } - last_status = status; + + last_status = overall_state; label_.set_markup(fmt::format( fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), - fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user))); + fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user), + fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), + fmt::arg("overall_state", overall_state))); ALabel::update(); } diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index ec0f585e..3fdd4eea 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -75,7 +75,7 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); } - // check if file_path_ can be used to retrive the temperature + // check if file_path_ can be used to retrieve the temperature std::ifstream temp(file_path_); if (!temp.is_open()) { throw std::runtime_error("Can't open " + file_path_); @@ -99,12 +99,17 @@ auto waybar::modules::Temperature::update() -> void { uint16_t temperature_f = std::round(temperature * 1.8 + 32); uint16_t temperature_k = std::round(temperature + 273.15); auto critical = isCritical(temperature_c); + auto warning = isWarning(temperature_c); auto format = format_; if (critical) { format = config_["format-critical"].isString() ? config_["format-critical"].asString() : format; label_.get_style_context()->add_class("critical"); + } else if (warning) { + format = config_["format-warning"].isString() ? config_["format-warning"].asString() : format; + label_.get_style_context()->add_class("warning"); } else { label_.get_style_context()->remove_class("critical"); + label_.get_style_context()->remove_class("warning"); } if (format.empty()) { @@ -139,13 +144,17 @@ float waybar::modules::Temperature::getTemperature() { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; - if (sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size, - NULL, 0) != 0) { - throw std::runtime_error(fmt::format( - "sysctl hw.acpi.thermal.tz{}.temperature or dev.cpu.{}.temperature failed", zone, zone)); + // First, try with dev.cpu + if ((sysctlbyname(fmt::format("dev.cpu.{}.temperature", zone).c_str(), &temp, &size, NULL, 0) == + 0) || + (sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size, + NULL, 0) == 0)) { + auto temperature_c = ((float)temp - 2732) / 10; + return temperature_c; } - auto temperature_c = ((float)temp - 2732) / 10; - return temperature_c; + + throw std::runtime_error(fmt::format( + "sysctl hw.acpi.thermal.tz{}.temperature and dev.cpu.{}.temperature failed", zone, zone)); #else // Linux std::ifstream temp(file_path_); @@ -165,6 +174,11 @@ float waybar::modules::Temperature::getTemperature() { #endif } +bool waybar::modules::Temperature::isWarning(uint16_t temperature_c) { + return config_["warning-threshold"].isInt() && + temperature_c >= config_["warning-threshold"].asInt(); +} + bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 5ee6d64c..4b832b7e 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -358,10 +358,12 @@ void UPower::resetDevices() { void UPower::setDisplayDevice() { std::lock_guard guard{mutex_}; - if (nativePath_.empty() && model_.empty()) { - // Unref current upDevice - if (upDevice_.upDevice != NULL) g_object_unref(upDevice_.upDevice); + if (upDevice_.upDevice != NULL) { + g_object_unref(upDevice_.upDevice); + upDevice_.upDevice = NULL; + } + if (nativePath_.empty() && model_.empty()) { upDevice_.upDevice = up_client_get_display_device(upClient_); getUpDeviceInfo(upDevice_); } else { @@ -386,7 +388,6 @@ void UPower::setDisplayDevice() { } // Unref current upDevice if it exists if (displayDevice.upDevice != NULL) { - if (thisPtr->upDevice_.upDevice != NULL) g_object_unref(thisPtr->upDevice_.upDevice); thisPtr->upDevice_ = displayDevice; } }, diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp new file mode 100644 index 00000000..5a9c0c1a --- /dev/null +++ b/src/modules/wayfire/backend.cpp @@ -0,0 +1,445 @@ +#include "modules/wayfire/backend.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace waybar::modules::wayfire { + +std::weak_ptr IPC::instance; + +// C++23: std::byteswap +inline auto byteswap(uint32_t x) -> uint32_t { + return (x & 0xff000000) >> 24 | (x & 0x00ff0000) >> 8 | (x & 0x0000ff00) << 8 | + (x & 0x000000ff) << 24; +} + +auto pack_and_write(Sock& sock, std::string&& buf) -> void { + uint32_t len = buf.size(); + if constexpr (std::endian::native != std::endian::little) len = byteswap(len); + (void)write(sock.fd, &len, 4); + (void)write(sock.fd, buf.data(), buf.size()); +} + +auto read_exact(Sock& sock, size_t n) -> std::string { + auto buf = std::string(n, 0); + for (size_t i = 0; i < n;) i += read(sock.fd, &buf[i], n - i); + return buf; +} + +// https://github.com/WayfireWM/pywayfire/blob/69b7c21/wayfire/ipc.py#L438 +inline auto is_mapped_toplevel_view(const Json::Value& view) -> bool { + return view["mapped"].asBool() && view["role"] != "desktop-environment" && + view["pid"].asInt() != -1; +} + +auto State::Wset::count_ws(const Json::Value& pos) -> Workspace& { + auto x = pos["x"].asInt(); + auto y = pos["y"].asInt(); + return wss.at(ws_w * y + x); +} + +auto State::Wset::locate_ws(const Json::Value& geo) -> Workspace& { + return const_cast(std::as_const(*this).locate_ws(geo)); +} + +auto State::Wset::locate_ws(const Json::Value& geo) const -> const Workspace& { + const auto& out = output.value().get(); + auto [qx, rx] = std::div(geo["x"].asInt(), out.w); + auto [qy, ry] = std::div(geo["y"].asInt(), out.h); + auto x = std::max(0, (int)ws_x + qx - int{rx < 0}); + auto y = std::max(0, (int)ws_y + qy - int{ry < 0}); + return wss.at(ws_w * y + x); +} + +auto State::update_view(const Json::Value& view) -> void { + auto id = view["id"].asUInt(); + + // erase old view information + if (views.contains(id)) { + auto& old_view = views.at(id); + auto& ws = wsets.at(old_view["wset-index"].asUInt()).locate_ws(old_view["geometry"]); + ws.num_views--; + if (old_view["sticky"].asBool()) ws.num_sticky_views--; + views.erase(id); + } + + // insert or assign new view information + if (is_mapped_toplevel_view(view)) { + try { + // view["wset-index"] could be messed up + auto& ws = wsets.at(view["wset-index"].asUInt()).locate_ws(view["geometry"]); + ws.num_views++; + if (view["sticky"].asBool()) ws.num_sticky_views++; + views.emplace(id, view); + } catch (const std::exception&) { + } + } +} + +auto IPC::get_instance() -> std::shared_ptr { + auto p = instance.lock(); + if (!p) instance = p = std::shared_ptr(new IPC); + return p; +} + +auto IPC::connect() -> Sock { + auto* path = std::getenv("WAYFIRE_SOCKET"); + if (path == nullptr) { + throw std::runtime_error{"Wayfire IPC: ipc not available"}; + } + + auto sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) { + throw std::runtime_error{"Wayfire IPC: socket() failed"}; + } + + auto addr = sockaddr_un{.sun_family = AF_UNIX}; + std::strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + if (::connect(sock, (const sockaddr*)&addr, sizeof(addr)) == -1) { + close(sock); + throw std::runtime_error{"Wayfire IPC: connect() failed"}; + } + + return {sock}; +} + +auto IPC::receive(Sock& sock) -> Json::Value { + auto len = *reinterpret_cast(read_exact(sock, 4).data()); + if constexpr (std::endian::native != std::endian::little) len = byteswap(len); + auto buf = read_exact(sock, len); + + Json::Value json; + std::string err; + auto* reader = reader_builder.newCharReader(); + if (!reader->parse(&*buf.begin(), &*buf.end(), &json, &err)) { + throw std::runtime_error{"Wayfire IPC: parse json failed: " + err}; + } + return json; +} + +auto IPC::send(const std::string& method, Json::Value&& data) -> Json::Value { + spdlog::debug("Wayfire IPC: send method \"{}\"", method); + auto sock = connect(); + + Json::Value json; + json["method"] = method; + json["data"] = std::move(data); + + pack_and_write(sock, Json::writeString(writer_builder, json)); + auto res = receive(sock); + root_event_handler(method, res); + return res; +} + +auto IPC::start() -> void { + spdlog::info("Wayfire IPC: starting"); + + // init state + send("window-rules/list-outputs", {}); + send("window-rules/list-wsets", {}); + send("window-rules/list-views", {}); + send("window-rules/get-focused-view", {}); + send("window-rules/get-focused-output", {}); + + std::thread([&] { + auto sock = connect(); + + { + Json::Value json; + json["method"] = "window-rules/events/watch"; + + pack_and_write(sock, Json::writeString(writer_builder, json)); + if (receive(sock)["result"] != "ok") { + spdlog::error( + "Wayfire IPC: method \"window-rules/events/watch\"" + " have failed"); + return; + } + } + + while (auto json = receive(sock)) { + auto ev = json["event"].asString(); + spdlog::debug("Wayfire IPC: received event \"{}\"", ev); + root_event_handler(ev, json); + } + }).detach(); +} + +auto IPC::register_handler(const std::string& event, const EventHandler& handler) -> void { + auto _ = std::lock_guard{handlers_mutex}; + handlers.emplace_back(event, handler); +} + +auto IPC::unregister_handler(EventHandler& handler) -> void { + auto _ = std::lock_guard{handlers_mutex}; + handlers.remove_if([&](auto& e) { return &e.second.get() == &handler; }); +} + +auto IPC::root_event_handler(const std::string& event, const Json::Value& data) -> void { + bool new_output_detected; + { + auto _ = lock_state(); + update_state_handler(event, data); + new_output_detected = state.new_output_detected; + state.new_output_detected = false; + } + if (new_output_detected) { + send("window-rules/list-outputs", {}); + send("window-rules/list-wsets", {}); + } + { + auto _ = std::lock_guard{handlers_mutex}; + for (const auto& [_event, handler] : handlers) + if (_event == event) handler(event); + } +} + +auto IPC::update_state_handler(const std::string& event, const Json::Value& data) -> void { + // IPC events + // https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-events.hpp#L108-L125 + /* + [x] view-mapped + [x] view-unmapped + [-] view-set-output // for detect new output + [ ] view-geometry-changed // -> view-workspace-changed + [x] view-wset-changed + [x] view-focused + [x] view-title-changed + [x] view-app-id-changed + [x] plugin-activation-state-changed + [x] output-gain-focus + + [ ] view-tiled + [ ] view-minimized + [ ] view-fullscreened + [x] view-sticky + [x] view-workspace-changed + [x] output-wset-changed + [x] wset-workspace-changed + */ + + if (event == "view-mapped") { + // data: { event, view } + state.update_view(data["view"]); + return; + } + + if (event == "view-unmapped") { + // data: { event, view } + try { + // data["view"]["wset-index"] could be messed up + state.update_view(data["view"]); + state.maybe_empty_focus_wset_idx = data["view"]["wset-index"].asUInt(); + } catch (const std::exception&) { + } + return; + } + + if (event == "view-set-output") { + // data: { event, output?, view } + // new output event + if (!state.outputs.contains(data["view"]["output-name"].asString())) { + state.new_output_detected = true; + } + return; + } + + if (event == "view-wset-changed") { + // data: { event, old-wset: wset, new-wset: wset, view } + state.maybe_empty_focus_wset_idx = data["old-wset"]["index"].asUInt(); + state.update_view(data["view"]); + return; + } + + if (event == "view-focused") { + // data: { event, view? } + if (const auto& view = data["view"]) { + try { + // view["wset-index"] could be messed up + auto& wset = state.wsets.at(view["wset-index"].asUInt()); + wset.focused_view_id = view["id"].asUInt(); + } catch (const std::exception&) { + } + } else { + // focused to null + if (state.wsets.contains(state.maybe_empty_focus_wset_idx)) + state.wsets.at(state.maybe_empty_focus_wset_idx).focused_view_id = {}; + } + return; + } + + if (event == "view-title-changed" || event == "view-app-id-changed" || event == "view-sticky") { + // data: { event, view } + state.update_view(data["view"]); + return; + } + + if (event == "plugin-activation-state-changed") { + // data: { event, plugin: name, state: bool, output: id, output-data: output } + auto plugin = data["plugin"].asString(); + auto plugin_state = data["state"].asBool(); + + if (plugin == "vswitch") { + state.vswitching = plugin_state; + if (plugin_state) { + state.maybe_empty_focus_wset_idx = data["output-data"]["wset-index"].asUInt(); + } + } + + return; + } + + if (event == "output-gain-focus") { + // data: { event, output } + state.focused_output_name = data["output"]["name"].asString(); + return; + } + + if (event == "view-workspace-changed") { + // data: { event, from: point, to: point, view } + if (state.vswitching) { + if (state.vswitch_sticky_view_id == 0) { + auto& wset = state.wsets.at(data["view"]["wset-index"].asUInt()); + auto& old_ws = wset.locate_ws(state.views.at(data["view"]["id"].asUInt())["geometry"]); + auto& new_ws = wset.count_ws(data["to"]); + old_ws.num_views--; + new_ws.num_views++; + if (data["view"]["sticky"].asBool()) { + old_ws.num_sticky_views--; + new_ws.num_sticky_views++; + } + state.update_view(data["view"]); + state.vswitch_sticky_view_id = data["view"]["id"].asUInt(); + } else { + state.vswitch_sticky_view_id = {}; + } + return; + } + state.update_view(data["view"]); + return; + } + + if (event == "output-wset-changed") { + // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + return; + } + + if (event == "wset-workspace-changed") { + // data: { event, previous-workspace: point, new-workspace: point, + // output: id, wset: wset.name, output-data: output, wset-data: wset } + auto wset_idx = data["wset-data"]["index"].asUInt(); + auto& wset = state.wsets.at(wset_idx); + wset.ws_x = data["new-workspace"]["x"].asUInt(); + wset.ws_y = data["new-workspace"]["y"].asUInt(); + + // correct existing views geometry + auto& out = wset.output.value().get(); + auto dx = (int)out.w * ((int)wset.ws_x - data["previous-workspace"]["x"].asInt()); + auto dy = (int)out.h * ((int)wset.ws_y - data["previous-workspace"]["y"].asInt()); + for (auto& [_, view] : state.views) { + if (view["wset-index"].asUInt() == wset_idx && + view["id"].asUInt() != state.vswitch_sticky_view_id) { + view["geometry"]["x"] = view["geometry"]["x"].asInt() - dx; + view["geometry"]["y"] = view["geometry"]["y"].asInt() - dy; + } + } + return; + } + + // IPC responses + // https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-rules.cpp#L27-L37 + + if (event == "window-rules/list-views") { + // data: [ view ] + state.views.clear(); + for (auto& [_, wset] : state.wsets) std::ranges::fill(wset.wss, State::Workspace{}); + for (const auto& view : data | std::views::filter(is_mapped_toplevel_view)) { + state.update_view(view); + } + return; + } + + if (event == "window-rules/list-outputs") { + // data: [ output ] + state.outputs.clear(); + for (const auto& output_data : data) { + state.outputs.emplace(output_data["name"].asString(), + State::Output{ + .id = output_data["id"].asUInt(), + .w = output_data["geometry"]["width"].asUInt(), + .h = output_data["geometry"]["height"].asUInt(), + .wset_idx = output_data["wset-index"].asUInt(), + }); + } + return; + } + + if (event == "window-rules/list-wsets") { + // data: [ wset ] + std::unordered_map wsets; + for (const auto& wset_data : data) { + auto wset_idx = wset_data["index"].asUInt(); + + auto output_name = wset_data["output-name"].asString(); + auto output = state.outputs.contains(output_name) + ? std::optional{std::ref(state.outputs.at(output_name))} + : std::nullopt; + + const auto& ws_data = wset_data["workspace"]; + auto ws_w = ws_data["grid_width"].asUInt(); + auto ws_h = ws_data["grid_height"].asUInt(); + + wsets.emplace(wset_idx, State::Wset{ + .output = output, + .wss = std::vector(ws_w * ws_h), + .ws_w = ws_w, + .ws_h = ws_h, + .ws_x = ws_data["x"].asUInt(), + .ws_y = ws_data["y"].asUInt(), + }); + + if (state.wsets.contains(wset_idx)) { + auto& old_wset = state.wsets.at(wset_idx); + auto& new_wset = wsets.at(wset_idx); + new_wset.wss = std::move(old_wset.wss); + new_wset.focused_view_id = old_wset.focused_view_id; + } + } + state.wsets = std::move(wsets); + return; + } + + if (event == "window-rules/get-focused-view") { + // data: { ok, info: view? } + if (const auto& view = data["info"]) { + auto& wset = state.wsets.at(view["wset-index"].asUInt()); + wset.focused_view_id = view["id"].asUInt(); + state.update_view(view); + } + return; + } + + if (event == "window-rules/get-focused-output") { + // data: { ok, info: output } + state.focused_output_name = data["info"]["name"].asString(); + return; + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wayfire/window.cpp b/src/modules/wayfire/window.cpp new file mode 100644 index 00000000..fbcde6ec --- /dev/null +++ b/src/modules/wayfire/window.cpp @@ -0,0 +1,77 @@ +#include "modules/wayfire/window.hpp" + +#include +#include +#include + +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::wayfire { + +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), + ipc{IPC::get_instance()}, + handler{[this](const auto&) { dp.emit(); }}, + bar_{bar} { + ipc->register_handler("view-unmapped", handler); + ipc->register_handler("view-focused", handler); + ipc->register_handler("view-title-changed", handler); + ipc->register_handler("view-app-id-changed", handler); + + ipc->register_handler("window-rules/get-focused-view", handler); + + dp.emit(); +} + +Window::~Window() { ipc->unregister_handler(handler); } + +auto Window::update() -> void { + update_icon_label(); + AAppIconLabel::update(); +} + +auto Window::update_icon_label() -> void { + auto _ = ipc->lock_state(); + + const auto& output = ipc->get_outputs().at(bar_.output->name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + const auto& views = ipc->get_views(); + auto ctx = bar_.window.get_style_context(); + + if (views.contains(wset.focused_view_id)) { + const auto& view = views.at(wset.focused_view_id); + auto title = view["title"].asString(); + auto app_id = view["app-id"].asString(); + + // update label + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", waybar::util::sanitize_string(title)), + fmt::arg("app_id", waybar::util::sanitize_string(app_id))), + config_["rewrite"])); + + // update window#waybar.solo + if (wset.locate_ws(view["geometry"]).num_views > 1) + ctx->remove_class("solo"); + else + ctx->add_class("solo"); + + // update window#waybar. + ctx->remove_class(old_app_id_); + ctx->add_class(old_app_id_ = app_id); + + // update window#waybar.empty + ctx->remove_class("empty"); + + // + updateAppIconName(app_id, ""); + label_.show(); + } else { + ctx->add_class("empty"); + + updateAppIconName("", ""); + label_.hide(); + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wayfire/workspaces.cpp b/src/modules/wayfire/workspaces.cpp new file mode 100644 index 00000000..6814004e --- /dev/null +++ b/src/modules/wayfire/workspaces.cpp @@ -0,0 +1,183 @@ +#include "modules/wayfire/workspaces.hpp" + +#include +#include +#include + +#include +#include + +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) + : AModule{config, "workspaces", id, false, !config["disable-scroll"].asBool()}, + ipc{IPC::get_instance()}, + handler{[this](const auto&) { dp.emit(); }}, + bar_{bar} { + // init box_ + box_.set_name("workspaces"); + if (!id.empty()) box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + // scroll events + if (!config_["disable-scroll"].asBool()) { + auto& target = config_["enable-bar-scroll"].asBool() ? const_cast(bar_).window + : dynamic_cast(box_); + target.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + target.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + + // listen events + ipc->register_handler("view-mapped", handler); + ipc->register_handler("view-unmapped", handler); + ipc->register_handler("view-wset-changed", handler); + ipc->register_handler("output-gain-focus", handler); + ipc->register_handler("view-sticky", handler); + ipc->register_handler("view-workspace-changed", handler); + ipc->register_handler("output-wset-changed", handler); + ipc->register_handler("wset-workspace-changed", handler); + + ipc->register_handler("window-rules/list-views", handler); + ipc->register_handler("window-rules/list-outputs", handler); + ipc->register_handler("window-rules/list-wsets", handler); + ipc->register_handler("window-rules/get-focused-output", handler); + + // initial render + dp.emit(); +} + +Workspaces::~Workspaces() { ipc->unregister_handler(handler); } + +auto Workspaces::handleScroll(GdkEventScroll* e) -> bool { + // Ignore emulated scroll events on window + if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) return false; + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) return true; + + int delta; + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) + delta = 1; + else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) + delta = -1; + else + return true; + + // cycle workspace + Json::Value data; + { + auto _ = ipc->lock_state(); + const auto& output = ipc->get_outputs().at(bar_.output->name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto n = wset.ws_w * wset.ws_h; + auto i = (wset.ws_idx() + delta + n) % n; + data["x"] = i % wset.ws_w; + data["y"] = i / wset.ws_h; + data["output-id"] = output.id; + } + ipc->send("vswitch/set-workspace", std::move(data)); + + return true; +} + +auto Workspaces::update() -> void { + update_box(); + AModule::update(); +} + +auto Workspaces::update_box() -> void { + auto _ = ipc->lock_state(); + + const auto& output_name = bar_.output->name; + const auto& output = ipc->get_outputs().at(output_name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + + auto output_focused = ipc->get_focused_output_name() == output_name; + auto ws_w = wset.ws_w; + auto ws_h = wset.ws_h; + auto num_wss = ws_w * ws_h; + + // add buttons for new workspaces + for (auto i = buttons_.size(); i < num_wss; i++) { + auto& btn = buttons_.emplace_back(""); + box_.pack_start(btn, false, false, 0); + btn.set_relief(Gtk::RELIEF_NONE); + if (!config_["disable-click"].asBool()) { + btn.signal_pressed().connect([=, this] { + Json::Value data; + data["x"] = i % ws_w; + data["y"] = i / ws_h; + data["output-id"] = output.id; + ipc->send("vswitch/set-workspace", std::move(data)); + }); + } + } + + // remove buttons for removed workspaces + buttons_.resize(num_wss); + + // update buttons + for (size_t i = 0; i < num_wss; i++) { + const auto& ws = wset.wss[i]; + auto& btn = buttons_[i]; + auto ctx = btn.get_style_context(); + auto ws_focused = i == wset.ws_idx(); + auto ws_empty = ws.num_views == 0; + + // update #workspaces button.focused + if (ws_focused) + ctx->add_class("focused"); + else + ctx->remove_class("focused"); + + // update #workspaces button.empty + if (ws_empty) + ctx->add_class("empty"); + else + ctx->remove_class("empty"); + + // update #workspaces button.current_output + if (output_focused) + ctx->add_class("current_output"); + else + ctx->remove_class("current_output"); + + // update label + auto label = std::to_string(i + 1); + if (config_["format"].isString()) { + auto format = config_["format"].asString(); + auto ws_idx = std::to_string(i + 1); + + const auto& icons = config_["format-icons"]; + std::string icon; + if (!icons) + icon = ws_idx; + else if (ws_focused && icons["focused"]) + icon = icons["focused"].asString(); + else if (icons[ws_idx]) + icon = icons[ws_idx].asString(); + else if (icons["default"]) + icon = icons["default"].asString(); + else + icon = ws_idx; + + label = fmt::format(fmt::runtime(format), fmt::arg("icon", icon), fmt::arg("index", ws_idx), + fmt::arg("output", output_name)); + } + if (!config_["disable-markup"].asBool()) + static_cast(btn.get_children()[0])->set_markup(label); + else + btn.set_label(label); + + // + if (config_["current-only"].asBool() && i != wset.ws_idx()) + btn.hide(); + else + btn.show(); + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index eddc3e6b..106ca403 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -4,6 +4,8 @@ bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } +std::list waybar::modules::Wireplumber::modules; + waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config) : ALabel(config, "wireplumber", id, "{volume}%"), wp_core_(nullptr), @@ -16,22 +18,28 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val muted_(false), volume_(0.0), min_step_(0.0), - node_id_(0) { + node_id_(0), + type_(nullptr) { + waybar::modules::Wireplumber::modules.push_back(this); + wp_init(WP_INIT_PIPEWIRE); wp_core_ = wp_core_new(nullptr, nullptr, nullptr); apis_ = g_ptr_array_new_with_free_func(g_object_unref); om_ = wp_object_manager_new(); - prepare(); + type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str() + : "Audio/Sink"); - spdlog::debug("[{}]: connecting to pipewire...", name_); + prepare(this); + + spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_); if (wp_core_connect(wp_core_) == 0) { - spdlog::error("[{}]: Could not connect to PipeWire", name_); + spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_); throw std::runtime_error("Could not connect to PipeWire\n"); } - spdlog::debug("[{}]: connected!", name_); + spdlog::debug("[{}]: {} connected!", name_, type_); g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this); @@ -39,6 +47,7 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val } waybar::modules::Wireplumber::~Wireplumber() { + waybar::modules::Wireplumber::modules.remove(this); wp_core_disconnect(wp_core_); g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); @@ -46,13 +55,15 @@ waybar::modules::Wireplumber::~Wireplumber() { g_clear_object(&mixer_api_); g_clear_object(&def_nodes_api_); g_free(default_node_name_); + g_free(type_); } void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self, uint32_t id) { - spdlog::debug("[{}]: updating node name with node.id {}", self->name_, id); + spdlog::debug("[{}]: updating '{}' node name with node.id {}", self->name_, self->type_, id); if (!isValidNodeId(id)) { - spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node name update.", self->name_, id); + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node name update.", self->name_, + id, self->type_); return; } @@ -80,7 +91,7 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self->node_name_ = nick != nullptr ? nick : description != nullptr ? description : "Unknown node name"; - spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_); + spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); } void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) { @@ -88,7 +99,8 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se GVariant* variant = nullptr; if (!isValidNodeId(id)) { - spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id); + spdlog::error("[{}]: '{}' is not a valid '{}' node ID. Ignoring volume update.", self->name_, + id, self->type_); return; } @@ -109,13 +121,22 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se } void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) { - spdlog::debug("[{}]: (onMixerChanged) - id: {}", self->name_, id); - g_autoptr(WpNode) node = static_cast(wp_object_manager_lookup( self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr)); if (node == nullptr) { - spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id); + // log a warning only if no other widget is targeting the id. + // this reduces log spam when multiple instances of the module are used on different node types. + if (id != self->node_id_) { + for (auto const& module : waybar::modules::Wireplumber::modules) { + if (module->node_id_ == id) { + return; + } + } + } + + spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, + self->type_, id); return; } @@ -123,26 +144,27 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* if (self->node_id_ != id) { spdlog::debug( - "[{}]: (onMixerChanged) - ignoring mixer update for node: id: {}, name: {} as it is not " - "the default node: {} with id: {}", - self->name_, id, name, self->default_node_name_, self->node_id_); + "[{}]: (onMixerChanged: {}) - ignoring mixer update for node: id: {}, name: {} as it is " + "not the default node: {} with id: {}", + self->name_, self->type_, id, name, self->default_node_name_, self->node_id_); return; } - spdlog::debug("[{}]: (onMixerChanged) - Need to update volume for node with id {} and name {}", - self->name_, id, name); + spdlog::debug( + "[{}]: (onMixerChanged: {}) - Need to update volume for node with id {} and name {}", + self->name_, self->type_, id, name); updateVolume(self, id); } void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) { - spdlog::debug("[{}]: (onDefaultNodesApiChanged)", self->name_); + spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_); uint32_t defaultNodeId; - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &defaultNodeId); + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId); if (!isValidNodeId(defaultNodeId)) { - spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_, - defaultNodeId); + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node change.", self->name_, + defaultNodeId, self->type_); return; } @@ -151,8 +173,8 @@ void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wir "=u", defaultNodeId, nullptr)); if (node == nullptr) { - spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_, - defaultNodeId); + spdlog::warn("[{}]: (onDefaultNodesApiChanged: {}) - Object with id {} not found", self->name_, + self->type_, defaultNodeId); return; } @@ -160,21 +182,22 @@ void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wir wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - got the following default node: Node(name: {}, id: {})", - self->name_, defaultNodeName, defaultNodeId); + "[{}]: (onDefaultNodesApiChanged: {}) - got the following default node: Node(name: {}, id: " + "{})", + self->name_, self->type_, defaultNodeName, defaultNodeId); if (g_strcmp0(self->default_node_name_, defaultNodeName) == 0 && self->node_id_ == defaultNodeId) { spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). " - "Ignoring.", - self->name_, self->default_node_name_, defaultNodeId); + "[{}]: (onDefaultNodesApiChanged: {}) - Default node has not changed. Node(name: {}, id: " + "{}). Ignoring.", + self->name_, self->type_, self->default_node_name_, defaultNodeId); return; } spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})", - self->name_, defaultNodeName, defaultNodeId); + "[{}]: (onDefaultNodesApiChanged: {}) - Default node changed to -> Node(name: {}, id: {})", + self->name_, self->type_, defaultNodeName, defaultNodeId); g_free(self->default_node_name_); self->default_node_name_ = g_strdup(defaultNodeName); @@ -200,13 +223,14 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir throw std::runtime_error("Mixer api is not loaded\n"); } - g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Sink", + g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_, &self->default_node_name_); - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &self->node_id_); + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_); if (self->default_node_name_ != nullptr) { - spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}", - self->name_, self->default_node_name_, self->node_id_); + spdlog::debug( + "[{}]: (onObjectManagerInstalled: {}) - default configured node name: {} and id: {}", + self->name_, self->type_, self->default_node_name_, self->node_id_); } updateVolume(self, self->node_id_); @@ -243,10 +267,10 @@ void waybar::modules::Wireplumber::activatePlugins() { } } -void waybar::modules::Wireplumber::prepare() { - spdlog::debug("[{}]: preparing object manager", name_); +void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { + spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", "Audio/Sink", nullptr); + "=s", self->type_, nullptr); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -275,7 +299,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r gboolean success = FALSE; g_autoptr(GError) error = nullptr; - success = wp_core_load_component_finish(self->wp_core_, res, nullptr); + success = wp_core_load_component_finish(self->wp_core_, res, &error); if (success == FALSE) { spdlog::error("[{}]: mixer API load failed", self->name_); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 30e4ee48..589638e8 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -383,8 +383,38 @@ std::string Task::state_string(bool shortened) const { } void Task::handle_title(const char *title) { + if (title_.empty()) { + spdlog::debug(fmt::format("Task ({}) setting title to {}", id_, title_)); + } else { + spdlog::debug(fmt::format("Task ({}) overwriting title '{}' with '{}'", id_, title_, title)); + } title_ = title; hide_if_ignored(); + + if (!with_icon_ && !with_name_ || app_info_) { + return; + } + + set_app_info_from_app_id_list(title_); + name_ = app_info_ ? app_info_->get_display_name() : title; + + if (!with_icon_) { + return; + } + + int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; + bool found = false; + for (auto &icon_theme : tbar_->icon_themes()) { + if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) { + found = true; + break; + } + } + + if (found) + icon_.show(); + else + spdlog::debug("Couldn't find icon for {}", title_); } void Task::set_minimize_hint() { @@ -682,6 +712,9 @@ void Task::update() { fmt::format(fmt::runtime(format_tooltip_), fmt::arg("title", title), fmt::arg("name", name), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true))); + + txt = waybar::util::rewriteString(txt, config_["rewrite"]); + if (markup) button.set_tooltip_markup(txt); else diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp index 3c630d81..f556a161 100644 --- a/src/modules/wlr/workspace_manager.cpp +++ b/src/modules/wlr/workspace_manager.cpp @@ -118,8 +118,8 @@ auto WorkspaceManager::sort_workspaces() -> void { } } -auto WorkspaceManager::register_manager(wl_registry *registry, uint32_t name, - uint32_t version) -> void { +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; diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 3d90b6d5..860168fd 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -1,9 +1,12 @@ #include "util/audio_backend.hpp" #include +#include #include +#include #include #include +#include #include #include @@ -21,6 +24,8 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc source_volume_(0), source_muted_(false), on_updated_cb_(std::move(on_updated_cb)) { + // Initialize pa_volume_ with safe defaults + pa_cvolume_init(&pa_volume_); mainloop_ = pa_threaded_mainloop_new(); if (mainloop_ == nullptr) { throw std::runtime_error("pa_mainloop_new() failed."); @@ -128,7 +133,12 @@ void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t void AudioBackend::volumeModifyCb(pa_context *c, int success, void *data) { auto *backend = static_cast(data); if (success != 0) { - pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + if ((backend->context_ != nullptr) && + pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { + pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + } + } else { + spdlog::debug("Volume modification failed"); } } @@ -139,6 +149,10 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i void *data) { if (i == nullptr) return; + auto running = i->state == PA_SINK_RUNNING; + auto idle = i->state == PA_SINK_IDLE; + spdlog::trace("Sink name {} Running:[{}] Idle:[{}]", i->name, running, idle); + auto *backend = static_cast(data); if (!backend->ignored_sinks_.empty()) { @@ -155,21 +169,38 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i } } - if (backend->current_sink_name_ == i->name) { - backend->current_sink_running_ = i->state == PA_SINK_RUNNING; + backend->default_sink_running_ = backend->default_sink_name == i->name && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + + if (i->name != backend->default_sink_name && !backend->default_sink_running_) { + return; } - if (!backend->current_sink_running_ && i->state == PA_SINK_RUNNING) { + if (backend->current_sink_name_ == i->name) { + backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + } + + if (!backend->current_sink_running_ && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) { backend->current_sink_name_ = i->name; backend->current_sink_running_ = true; } if (backend->current_sink_name_ == i->name) { - backend->pa_volume_ = i->volume; - float volume = - static_cast(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM}; - backend->sink_idx_ = i->index; - backend->volume_ = std::round(volume * 100.0F); + // Safely copy the volume structure + if (pa_cvolume_valid(&i->volume) != 0) { + backend->pa_volume_ = i->volume; + float volume = + static_cast(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM}; + backend->sink_idx_ = i->index; + backend->volume_ = std::round(volume * 100.0F); + } else { + spdlog::error("Invalid volume structure received from PulseAudio"); + // Initialize with safe defaults + pa_cvolume_init(&backend->pa_volume_); + backend->volume_ = 0; + } + backend->muted_ = i->mute != 0; backend->desc_ = i->description; backend->monitor_ = i->monitor_source_name; @@ -207,6 +238,7 @@ void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info * void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) { auto *backend = static_cast(data); backend->current_sink_name_ = i->default_sink_name; + backend->default_sink_name = i->default_sink_name; backend->default_source_name_ = i->default_source_name; pa_context_get_sink_info_list(context, sinkInfoCb, data); @@ -214,66 +246,144 @@ void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, vo } void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { - double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_cvolume pa_volume = pa_volume_; + // Early return if context is not ready + if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { + spdlog::error("PulseAudio context not ready"); + return; + } + // Prepare volume structure + pa_cvolume pa_volume; + + pa_cvolume_init(&pa_volume); + + // Use existing volume structure if valid, otherwise create a safe default + if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { + pa_volume = pa_volume_; + } else { + // Set stereo as a safe default + pa_volume.channels = 2; + spdlog::debug("Using default stereo volume structure"); + } + + // Set the volume safely volume = std::clamp(volume, min_volume, max_volume); - pa_cvolume_set(&pa_volume, pa_volume_.channels, volume * volume_tick); + pa_volume_t vol = volume * (static_cast(PA_VOLUME_NORM) / 100); + // Set all channels to the same volume manually to avoid pa_cvolume_set + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = vol; + } + + // Apply the volume change + pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); } void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { - double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_volume_t change = volume_tick; - pa_cvolume pa_volume = pa_volume_; + // Early return if context is not ready + if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { + spdlog::error("PulseAudio context not ready"); + return; + } + // Prepare volume structure + pa_cvolume pa_volume; + pa_cvolume_init(&pa_volume); + + // Use existing volume structure if valid, otherwise create a safe default + if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { + pa_volume = pa_volume_; + } else { + // Set stereo as a safe default + pa_volume.channels = 2; + spdlog::debug("Using default stereo volume structure"); + + // Initialize all channels to current volume level + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t vol = volume_ * volume_tick; + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = vol; + } + + // No need to continue with volume change if we had to create a new structure + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); + return; + } + + // Calculate volume change + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t change; max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); - if (change_type == ChangeType::Increase) { - if (volume_ < max_volume) { - if (volume_ + step > max_volume) { - change = round((max_volume - volume_) * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_inc(&pa_volume, change); + if (change_type == ChangeType::Increase && volume_ < max_volume) { + // Calculate how much to increase + if (volume_ + step > max_volume) { + change = round((max_volume - volume_) * volume_tick); + } else { + change = round(step * volume_tick); } - } else if (change_type == ChangeType::Decrease) { - if (volume_ > 0) { - if (volume_ - step < 0) { - change = round(volume_ * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_dec(&pa_volume, change); + + // Manually increase each channel's volume + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = std::min(pa_volume.values[i] + change, PA_VOLUME_MAX); } + } else if (change_type == ChangeType::Decrease && volume_ > 0) { + // Calculate how much to decrease + if (volume_ - step < 0) { + change = round(volume_ * volume_tick); + } else { + change = round(step * volume_tick); + } + + // Manually decrease each channel's volume + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = (pa_volume.values[i] > change) ? (pa_volume.values[i] - change) : 0; + } + } else { + // No change needed + return; } + + // Apply the volume change + pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); } void AudioBackend::toggleSinkMute() { muted_ = !muted_; + pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); } void AudioBackend::toggleSinkMute(bool mute) { muted_ = mute; + pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); } void AudioBackend::toggleSourceMute() { source_muted_ = !muted_; + pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); } void AudioBackend::toggleSourceMute(bool mute) { source_muted_ = mute; + pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); } bool AudioBackend::isBluetooth() { diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index bb102cd9..863896d5 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -150,10 +150,18 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, throw std::runtime_error("No backlight found"); } +#ifdef HAVE_LOGIN_PROXY // Connect to the login interface login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.login1", - "/org/freedesktop/login1/session/self", "org.freedesktop.login1.Session"); + "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session"); + + if (!login_proxy_) { + login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.login1", + "/org/freedesktop/login1/session/self", "org.freedesktop.login1.Session"); + } +#endif udev_thread_ = [this] { std::unique_ptr udev{udev_new()}; diff --git a/src/util/gtk_icon.cpp b/src/util/gtk_icon.cpp index 5dd741f9..73f77284 100644 --- a/src/util/gtk_icon.cpp +++ b/src/util/gtk_icon.cpp @@ -15,11 +15,24 @@ bool DefaultGtkIconThemeWrapper::has_icon(const std::string& value) { return Gtk::IconTheme::get_default()->has_icon(value); } -Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon(const char* name, int tmp_size, - Gtk::IconLookupFlags flags) { +Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon( + const char* name, int tmp_size, Gtk::IconLookupFlags flags, + Glib::RefPtr style) { const std::lock_guard lock(default_theme_mutex); auto default_theme = Gtk::IconTheme::get_default(); default_theme->rescan_if_needed(); - return default_theme->load_icon(name, tmp_size, flags); + + auto icon_info = default_theme->lookup_icon(name, tmp_size, flags); + + if (icon_info == nullptr) { + return default_theme->load_icon(name, tmp_size, flags); + } + + if (style.get() == nullptr) { + return icon_info.load_icon(); + } + + bool is_sym = false; + return icon_info.load_symbolic(style, is_sym); } diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp index 5bb7c19a..6d859a91 100644 --- a/src/util/pipewire/pipewire_backend.cpp +++ b/src/util/pipewire/pipewire_backend.cpp @@ -126,6 +126,7 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission if (proxy == nullptr) return; auto *pNodeInfo = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + new (pNodeInfo) PrivacyNodeInfo{}; pNodeInfo->id = id; pNodeInfo->data = this; pNodeInfo->type = mediaType; @@ -142,6 +143,7 @@ void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { mutex_.lock(); auto iter = privacy_nodes.find(id); if (iter != privacy_nodes.end()) { + privacy_nodes[id]->~PrivacyNodeInfo(); privacy_nodes.erase(id); } mutex_.unlock(); diff --git a/src/util/pipewire/privacy_node_info.cpp b/src/util/pipewire/privacy_node_info.cpp index 739dc528..ec110b86 100644 --- a/src/util/pipewire/privacy_node_info.cpp +++ b/src/util/pipewire/privacy_node_info.cpp @@ -49,6 +49,8 @@ void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) { pipewire_access_portal_app_id = item->value; } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { application_icon_name = item->value; + } else if (strcmp(item->key, "stream.monitor") == 0) { + is_monitor = strcmp(item->value, "true") == 0; } } } diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 5874871b..6df2a6b6 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -17,8 +17,6 @@ static constexpr const char* PORTAL_NAMESPACE = "org.freedesktop.appearance"; static constexpr const char* PORTAL_KEY = "color-scheme"; } // namespace waybar -using namespace Gio; - auto fmt::formatter::format(waybar::Appearance c, format_context& ctx) const { string_view name; switch (c) { @@ -36,8 +34,8 @@ auto fmt::formatter::format(waybar::Appearance c, format_con } waybar::Portal::Portal() - : DBus::Proxy(DBus::Connection::get_sync(DBus::BusType::BUS_TYPE_SESSION), PORTAL_BUS_NAME, - PORTAL_OBJ_PATH, PORTAL_INTERFACE), + : Gio::DBus::Proxy(Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION), + PORTAL_BUS_NAME, PORTAL_OBJ_PATH, PORTAL_INTERFACE), currentMode(Appearance::UNKNOWN) { refreshAppearance(); }; diff --git a/src/util/regex_collection.cpp b/src/util/regex_collection.cpp index 929e67cd..51dd6ff7 100644 --- a/src/util/regex_collection.cpp +++ b/src/util/regex_collection.cpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace waybar::util { diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap index f0309bf5..f220207c 100644 --- a/subprojects/cava.wrap +++ b/subprojects/cava.wrap @@ -1,7 +1,7 @@ [wrap-file] -directory = cava-0.10.3 -source_url = https://github.com/LukashonakV/cava/archive/0.10.3.tar.gz -source_filename = cava-0.10.3.tar.gz -source_hash = aab0a4ed3f999e8461ad9de63ef8a77f28b6b2011f7dd0c69ba81819d442f6f9 +directory = cava-0.10.4 +source_url = https://github.com/LukashonakV/cava/archive/0.10.4.tar.gz +source_filename = cava-0.10.4.tar.gz +source_hash =7bc1c1f9535f2bcc5cd2ae8a2434a2e3a05f5670b1c96316df304137ffc65756 [provide] cava = cava_dep diff --git a/test/utils/date.cpp b/test/utils/date.cpp index d317f98a..576a4799 100644 --- a/test/utils/date.cpp +++ b/test/utils/date.cpp @@ -18,8 +18,10 @@ return #endif +using namespace date; using namespace std::literals::chrono_literals; namespace fmt_lib = waybar::util::date::format; + /* * Check that the date/time formatter with locale and timezone support is working as expected. */