Merge branch 'master' of github.com:Alexays/Waybar

This commit is contained in:
mazunki 2022-01-08 01:02:57 +01:00
commit d22d6a4522
50 changed files with 2822 additions and 357 deletions

View File

@ -4,12 +4,16 @@ on: [ push, pull_request ]
jobs: jobs:
clang: clang:
runs-on: macos-latest # until https://github.com/actions/runner/issues/385 # Run actions in a FreeBSD vm on the macos-10.15 runner
# https://github.com/actions/runner/issues/385 - for FreeBSD runner support
# https://github.com/actions/virtual-environments/issues/4060 - for lack of VirtualBox on MacOS 11 runners
runs-on: macos-10.15
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Test in FreeBSD VM - name: Test in FreeBSD VM
uses: vmactions/freebsd-vm@v0.1.4 # aka FreeBSD 12.2 uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0
with: with:
mem: 2048
usesh: true usesh: true
prepare: | prepare: |
export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio

View File

@ -2,7 +2,7 @@
> Highly customizable Wayland bar for Sway and Wlroots based compositors.<br> > Highly customizable Wayland bar for Sway and Wlroots based compositors.<br>
> Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or > Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or
[AUR](https://aur.archlinux.org/packages/waybar-git/), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)<br> [AUR](https://aur.archlinux.org/packages/waybar-git/), [Gentoo](https://packages.gentoo.org/packages/gui-apps/waybar), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)<br>
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* > *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)*
#### Current features #### Current features
@ -69,6 +69,7 @@ libdbusmenu-gtk3 [Tray module]
libmpdclient [MPD module] libmpdclient [MPD module]
libsndio [sndio module] libsndio [sndio module]
libevdev [KeyboardState module] libevdev [KeyboardState module]
xkbregistry
``` ```
**Build dependencies** **Build dependencies**
@ -101,7 +102,8 @@ sudo apt install \
libsigc++-2.0-dev \ libsigc++-2.0-dev \
libspdlog-dev \ libspdlog-dev \
libwayland-dev \ libwayland-dev \
scdoc scdoc \
libxkbregistry-dev
``` ```

View File

@ -8,6 +8,9 @@
#include <gtkmm/window.h> #include <gtkmm/window.h>
#include <json/json.h> #include <json/json.h>
#include <memory>
#include <vector>
#include "AModule.hpp" #include "AModule.hpp"
#include "xdg-output-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h"
@ -36,6 +39,19 @@ struct bar_margins {
int left = 0; int left = 0;
}; };
struct bar_mode {
bar_layer layer;
bool exclusive;
bool passthrough;
bool visible;
};
#ifdef HAVE_SWAY
namespace modules::sway {
class BarIpcClient;
}
#endif // HAVE_SWAY
class BarSurface { class BarSurface {
protected: protected:
BarSurface() = default; BarSurface() = default;
@ -54,38 +70,56 @@ class BarSurface {
class Bar { class Bar {
public: public:
using bar_mode_map = std::map<std::string_view, struct bar_mode>;
static const bar_mode_map PRESET_MODES;
static const std::string_view MODE_DEFAULT;
static const std::string_view MODE_INVISIBLE;
Bar(struct waybar_output *w_output, const Json::Value &); Bar(struct waybar_output *w_output, const Json::Value &);
Bar(const Bar &) = delete; Bar(const Bar &) = delete;
~Bar() = default; ~Bar();
void setMode(const std::string_view &);
void setVisible(bool visible); void setVisible(bool visible);
void toggle(); void toggle();
void handleSignal(int); void handleSignal(int);
struct waybar_output *output; struct waybar_output *output;
Json::Value config; Json::Value config;
struct wl_surface * surface; struct wl_surface *surface;
bool exclusive = true;
bool visible = true; bool visible = true;
bool vertical = false; bool vertical = false;
Gtk::Window window; Gtk::Window window;
#ifdef HAVE_SWAY
std::string bar_id;
#endif
private: private:
void onMap(GdkEventAny *); void onMap(GdkEventAny *);
auto setupWidgets() -> void; auto setupWidgets() -> void;
void getModules(const Factory &, const std::string &); void getModules(const Factory &, const std::string &, Gtk::Box*);
void setupAltFormatKeyForModule(const std::string &module_name); void setupAltFormatKeyForModule(const std::string &module_name);
void setupAltFormatKeyForModuleList(const char *module_list_name); void setupAltFormatKeyForModuleList(const char *module_list_name);
void setMode(const bar_mode &);
/* Copy initial set of modes to allow customization */
bar_mode_map configured_modes = PRESET_MODES;
std::string last_mode_{MODE_DEFAULT};
std::unique_ptr<BarSurface> surface_impl_; std::unique_ptr<BarSurface> surface_impl_;
bar_layer layer_;
Gtk::Box left_; Gtk::Box left_;
Gtk::Box center_; Gtk::Box center_;
Gtk::Box right_; Gtk::Box right_;
Gtk::Box box_; Gtk::Box box_;
std::vector<std::unique_ptr<waybar::AModule>> modules_left_; std::vector<std::shared_ptr<waybar::AModule>> modules_left_;
std::vector<std::unique_ptr<waybar::AModule>> modules_center_; std::vector<std::shared_ptr<waybar::AModule>> modules_center_;
std::vector<std::unique_ptr<waybar::AModule>> modules_right_; std::vector<std::shared_ptr<waybar::AModule>> modules_right_;
#ifdef HAVE_SWAY
using BarIpcClient = modules::sway::BarIpcClient;
std::unique_ptr<BarIpcClient> _ipc_client;
#endif
std::vector<std::shared_ptr<waybar::AModule>> modules_all_;
}; };
} // namespace waybar } // namespace waybar

View File

@ -29,6 +29,7 @@ class Client {
struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr;
std::vector<std::unique_ptr<Bar>> bars; std::vector<std::unique_ptr<Bar>> bars;
Config config; Config config;
std::string bar_id;
private: private:
Client() = default; Client() = default;

View File

@ -14,6 +14,7 @@
#endif #endif
#ifdef HAVE_WLR #ifdef HAVE_WLR
#include "modules/wlr/taskbar.hpp" #include "modules/wlr/taskbar.hpp"
#include "modules/wlr/workspace_manager.hpp"
#endif #endif
#ifdef HAVE_RIVER #ifdef HAVE_RIVER
#include "modules/river/tags.hpp" #include "modules/river/tags.hpp"
@ -50,6 +51,9 @@
#ifdef HAVE_LIBSNDIO #ifdef HAVE_LIBSNDIO
#include "modules/sndio.hpp" #include "modules/sndio.hpp"
#endif #endif
#ifdef HAVE_GIO_UNIX
#include "modules/inhibitor.hpp"
#endif
#include "bar.hpp" #include "bar.hpp"
#include "modules/custom.hpp" #include "modules/custom.hpp"
#include "modules/temperature.hpp" #include "modules/temperature.hpp"

21
include/group.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <gtkmm/widget.h>
#include <gtkmm/box.h>
#include <json/json.h>
#include "AModule.hpp"
#include "bar.hpp"
#include "factory.hpp"
namespace waybar {
class Group : public AModule {
public:
Group(const std::string&, const Bar&, const Json::Value&);
~Group() = default;
auto update() -> void;
operator Gtk::Widget &();
Gtk::Box box;
};
} // namespace waybar

View File

@ -17,6 +17,8 @@ struct waybar_time {
date::zoned_seconds ztime; date::zoned_seconds ztime;
}; };
const std::string kCalendarPlaceholder = "calendar";
class Clock : public ALabel { class Clock : public ALabel {
public: public:
Clock(const std::string&, const Json::Value&); Clock(const std::string&, const Json::Value&);
@ -26,18 +28,19 @@ class Clock : public ALabel {
private: private:
util::SleeperThread thread_; util::SleeperThread thread_;
std::locale locale_; std::locale locale_;
const date::time_zone* time_zone_; std::vector<const date::time_zone*> time_zones_;
bool fixed_time_zone_; int current_time_zone_idx_;
int time_zone_idx_;
date::year_month_day cached_calendar_ymd_ = date::January/1/0; date::year_month_day cached_calendar_ymd_ = date::January/1/0;
std::string cached_calendar_text_; std::string cached_calendar_text_;
bool is_calendar_in_tooltip_;
bool handleScroll(GdkEventScroll* e); bool handleScroll(GdkEventScroll* e);
auto calendar_text(const waybar_time& wtime) -> std::string; auto calendar_text(const waybar_time& wtime) -> std::string;
auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void; auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void;
auto first_day_of_week() -> date::weekday; auto first_day_of_week() -> date::weekday;
bool setTimeZone(Json::Value zone_name); const date::time_zone* current_timezone();
bool is_timezone_fixed();
}; };
} // namespace waybar::modules } // namespace waybar::modules

View File

@ -0,0 +1,27 @@
#pragma once
#include <memory>
#include <gio/gio.h>
#include "ALabel.hpp"
#include "bar.hpp"
namespace waybar::modules {
class Inhibitor : public ALabel {
public:
Inhibitor(const std::string&, const waybar::Bar&, const Json::Value&);
~Inhibitor() override;
auto update() -> void;
auto activated() -> bool;
private:
auto handleToggle(::GdkEventButton* const& e) -> bool;
const std::unique_ptr<::GDBusConnection, void(*)(::GDBusConnection*)> dbus_;
const std::string inhibitors_;
int handle_ = -1;
};
} // namespace waybar::modules

View File

@ -43,6 +43,7 @@ class Network : public ALabel {
const std::string getNetworkState() const; const std::string getNetworkState() const;
void clearIface(); void clearIface();
bool wildcardMatch(const std::string& pattern, const std::string& text) const; bool wildcardMatch(const std::string& pattern, const std::string& text) const;
std::optional<std::pair<unsigned long long, unsigned long long>> readBandwidthUsage();
int ifid_; int ifid_;
sa_family_t family_; sa_family_t family_;
@ -72,7 +73,8 @@ class Network : public ALabel {
int cidr_; int cidr_;
int32_t signal_strength_dbm_; int32_t signal_strength_dbm_;
uint8_t signal_strength_; uint8_t signal_strength_;
uint32_t frequency_; std::string signal_strength_app_;
float frequency_;
uint32_t route_priority; uint32_t route_priority;
util::SleeperThread thread_; util::SleeperThread thread_;

View File

@ -6,6 +6,7 @@
#include "AModule.hpp" #include "AModule.hpp"
#include "bar.hpp" #include "bar.hpp"
#include "river-status-unstable-v1-client-protocol.h" #include "river-status-unstable-v1-client-protocol.h"
#include "river-control-unstable-v1-client-protocol.h"
#include "xdg-output-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h"
namespace waybar::modules::river { namespace waybar::modules::river {
@ -20,7 +21,12 @@ class Tags : public waybar::AModule {
void handle_view_tags(struct wl_array *tags); void handle_view_tags(struct wl_array *tags);
void handle_urgent_tags(uint32_t tags); void handle_urgent_tags(uint32_t tags);
void handle_primary_clicked(uint32_t tag);
bool handle_button_press(GdkEventButton *event_button, uint32_t tag);
struct zriver_status_manager_v1 *status_manager_; struct zriver_status_manager_v1 *status_manager_;
struct zriver_control_v1 *control_;
struct wl_seat *seat_;
private: private:
const waybar::Bar & bar_; const waybar::Bar & bar_;

View File

@ -0,0 +1,49 @@
#pragma once
#include <string>
#include "modules/sway/ipc/client.hpp"
#include "util/SafeSignal.hpp"
#include "util/json.hpp"
namespace waybar {
class Bar;
namespace modules::sway {
/*
* Supported subset of i3/sway IPC barconfig object
*/
struct swaybar_config {
std::string id;
std::string mode;
std::string hidden_state;
};
/**
* swaybar IPC client
*/
class BarIpcClient {
public:
BarIpcClient(waybar::Bar& bar);
private:
void onInitialConfig(const struct Ipc::ipc_response& res);
void onIpcEvent(const struct Ipc::ipc_response&);
void onConfigUpdate(const swaybar_config& config);
void onVisibilityUpdate(bool visible_by_modifier);
void update();
Bar& bar_;
util::JsonParser parser_;
Ipc ipc_;
swaybar_config bar_config_;
bool visible_by_modifier_ = false;
SafeSignal<bool> signal_visible_;
SafeSignal<swaybar_config> signal_config_;
};
} // namespace modules::sway
} // namespace waybar

View File

@ -32,6 +32,7 @@ class Language : public ALabel, public sigc::trackable {
std::string short_name; std::string short_name;
std::string variant; std::string variant;
std::string short_description; std::string short_description;
std::string country_flag() const;
}; };
class XKBContext { class XKBContext {
@ -54,7 +55,7 @@ class Language : public ALabel, public sigc::trackable {
const static std::string XKB_LAYOUT_NAMES_KEY; const static std::string XKB_LAYOUT_NAMES_KEY;
const static std::string XKB_ACTIVE_LAYOUT_NAME_KEY; const static std::string XKB_ACTIVE_LAYOUT_NAME_KEY;
Layout layout_; Layout layout_;
std::string tooltip_format_ = ""; std::string tooltip_format_ = "";
std::map<std::string, Layout> layouts_map_; std::map<std::string, Layout> layouts_map_;

View File

@ -3,11 +3,13 @@
#include "AModule.hpp" #include "AModule.hpp"
#include "bar.hpp" #include "bar.hpp"
#include "client.hpp" #include "client.hpp"
#include "giomm/desktopappinfo.h"
#include "util/json.hpp" #include "util/json.hpp"
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include <map>
#include <unordered_set> #include <unordered_set>
#include <gdk/gdk.h> #include <gdk/gdk.h>
@ -61,15 +63,18 @@ class Task
Gtk::Image icon_; Gtk::Image icon_;
Gtk::Label text_before_; Gtk::Label text_before_;
Gtk::Label text_after_; Gtk::Label text_after_;
bool button_visible_; Glib::RefPtr<Gio::DesktopAppInfo> app_info_;
bool ignored_; bool button_visible_ = false;
bool ignored_ = false;
bool with_icon_; bool with_icon_ = false;
bool with_name_ = false;
std::string format_before_; std::string format_before_;
std::string format_after_; std::string format_after_;
std::string format_tooltip_; std::string format_tooltip_;
std::string name_;
std::string title_; std::string title_;
std::string app_id_; std::string app_id_;
uint32_t state_ = 0; uint32_t state_ = 0;
@ -77,6 +82,9 @@ class Task
private: private:
std::string repr() const; std::string repr() const;
std::string state_string(bool = false) const; std::string state_string(bool = false) const;
void set_app_info_from_app_id_list(const std::string& app_id_list);
bool image_load_icon(Gtk::Image& image, const Glib::RefPtr<Gtk::IconTheme>& icon_theme, Glib::RefPtr<Gio::DesktopAppInfo> app_info, int size);
void hide_if_ignored();
public: public:
/* Getter functions */ /* Getter functions */
@ -135,6 +143,7 @@ class Taskbar : public waybar::AModule
std::vector<Glib::RefPtr<Gtk::IconTheme>> icon_themes_; std::vector<Glib::RefPtr<Gtk::IconTheme>> icon_themes_;
std::unordered_set<std::string> ignore_list_; std::unordered_set<std::string> ignore_list_;
std::map<std::string, std::string> app_ids_replace_map_;
struct zwlr_foreign_toplevel_manager_v1 *manager_; struct zwlr_foreign_toplevel_manager_v1 *manager_;
struct wl_seat *seat_; struct wl_seat *seat_;
@ -157,8 +166,9 @@ class Taskbar : public waybar::AModule
bool show_output(struct wl_output *) const; bool show_output(struct wl_output *) const;
bool all_outputs() const; bool all_outputs() const;
std::vector<Glib::RefPtr<Gtk::IconTheme>> icon_themes() const; const std::vector<Glib::RefPtr<Gtk::IconTheme>>& icon_themes() const;
const std::unordered_set<std::string>& ignore_list() const; const std::unordered_set<std::string>& ignore_list() const;
const std::map<std::string, std::string>& app_ids_replace_map() const;
}; };
} /* namespace waybar::modules::wlr */ } /* namespace waybar::modules::wlr */

View File

@ -0,0 +1,160 @@
#pragma once
#include <fmt/format.h>
#include <gtkmm/button.h>
#include <gtkmm/image.h>
#include <gtkmm/label.h>
#include <functional>
#include <map>
#include <memory>
#include <vector>
#include "AModule.hpp"
#include "bar.hpp"
#include "ext-workspace-unstable-v1-client-protocol.h"
namespace waybar::modules::wlr {
class WorkspaceManager;
class WorkspaceGroup;
class Workspace {
public:
Workspace(const waybar::Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group,
zext_workspace_handle_v1 *workspace, uint32_t id);
~Workspace();
auto update() -> void;
auto id() const -> uint32_t { return id_; }
auto is_active() const -> bool { return state_ & static_cast<uint32_t>(State::ACTIVE); }
auto is_urgent() const -> bool { return state_ & static_cast<uint32_t>(State::URGENT); }
auto is_hidden() const -> bool { return state_ & static_cast<uint32_t>(State::HIDDEN); }
// wlr stuff
auto handle_name(const std::string &name) -> void;
auto handle_coordinates(const std::vector<uint32_t> &coordinates) -> void;
auto handle_state(const std::vector<uint32_t> &state) -> void;
auto handle_remove() -> void;
auto handle_done() -> void;
auto handle_clicked(GdkEventButton *bt) -> bool;
auto show() -> void;
auto hide() -> void;
auto get_button_ref() -> Gtk::Button & { return button_; }
auto get_name() -> std::string & { return name_; }
auto get_coords() -> std::vector<uint32_t> & { return coordinates_; }
enum class State {
ACTIVE = (1 << 0),
URGENT = (1 << 1),
HIDDEN = (1 << 2),
};
private:
auto get_icon() -> std::string;
const Bar &bar_;
const Json::Value &config_;
WorkspaceGroup &workspace_group_;
// wlr stuff
zext_workspace_handle_v1 *workspace_handle_;
uint32_t state_ = 0;
uint32_t id_;
std::string name_;
std::vector<uint32_t> coordinates_;
static std::map<std::string, std::string> icons_map_;
std::string format_;
bool with_icon_ = false;
Gtk::Button button_;
Gtk::Box content_;
Gtk::Label label_;
};
class WorkspaceGroup {
public:
WorkspaceGroup(const waybar::Bar &bar, Gtk::Box &box, const Json::Value &config,
WorkspaceManager &manager, zext_workspace_group_handle_v1 *workspace_group_handle,
uint32_t id);
~WorkspaceGroup();
auto update() -> void;
auto id() const -> uint32_t { return id_; }
auto is_visible() const -> bool;
auto remove_workspace(uint32_t id_) -> void;
auto active_only() const -> bool;
auto creation_delayed() const -> bool;
auto workspaces() -> std::vector<std::unique_ptr<Workspace>> & { return workspaces_; }
auto sort_workspaces() -> void;
auto set_need_to_sort() -> void { need_to_sort = true; }
auto add_button(Gtk::Button &button) -> void;
auto remove_button(Gtk::Button &button) -> void;
// wlr stuff
auto handle_workspace_create(zext_workspace_handle_v1 *workspace_handle) -> void;
auto handle_remove() -> void;
auto handle_output_enter(wl_output *output) -> void;
auto handle_output_leave() -> void;
auto handle_done() -> void;
auto commit() -> void;
private:
static uint32_t workspace_global_id;
const waybar::Bar &bar_;
Gtk::Box &box_;
const Json::Value &config_;
WorkspaceManager &workspace_manager_;
// wlr stuff
zext_workspace_group_handle_v1 *workspace_group_handle_;
wl_output *output_ = nullptr;
uint32_t id_;
std::vector<std::unique_ptr<Workspace>> workspaces_;
bool need_to_sort = false;
};
class WorkspaceManager : public AModule {
public:
WorkspaceManager(const std::string &id, const waybar::Bar &bar, const Json::Value &config);
~WorkspaceManager() override;
auto update() -> void override;
auto all_outputs() const -> bool { return all_outputs_; }
auto active_only() const -> bool { return active_only_; }
auto workspace_comparator() const
-> std::function<bool(std::unique_ptr<Workspace> &, std::unique_ptr<Workspace> &)>;
auto creation_delayed() const -> bool { return creation_delayed_; }
auto sort_workspaces() -> void;
auto remove_workspace_group(uint32_t id_) -> void;
// wlr stuff
auto register_manager(wl_registry *registry, uint32_t name, uint32_t version) -> void;
auto handle_workspace_group_create(zext_workspace_group_handle_v1 *workspace_group_handle)
-> void;
auto handle_done() -> void;
auto handle_finished() -> void;
auto commit() -> void;
private:
const waybar::Bar &bar_;
Gtk::Box box_;
std::vector<std::unique_ptr<WorkspaceGroup>> groups_;
// wlr stuff
zext_workspace_manager_v1 *workspace_manager_ = nullptr;
static uint32_t group_global_id;
bool sort_by_name_ = true;
bool sort_by_coordinates_ = true;
bool all_outputs_ = false;
bool active_only_ = false;
bool creation_delayed_ = false;
};
} // namespace waybar::modules::wlr

View File

@ -0,0 +1,8 @@
#include "ext-workspace-unstable-v1-client-protocol.h"
namespace waybar::modules::wlr {
void add_registry_listener(void *data);
void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data);
void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle, void *data);
zext_workspace_manager_v1* workspace_manager_bind(wl_registry *registry, uint32_t name, uint32_t version, void *data);
}

View File

@ -0,0 +1,75 @@
#pragma once
#include <glibmm/dispatcher.h>
#include <sigc++/signal.h>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
namespace waybar {
/**
* Thread-safe signal wrapper.
* Uses Glib::Dispatcher to pass events to another thread and locked queue to pass the arguments.
*/
template <typename... Args>
struct SafeSignal : sigc::signal<void(std::decay_t<Args>...)> {
public:
SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); }
template <typename... EmitArgs>
void emit(EmitArgs&&... args) {
if (main_tid_ == std::this_thread::get_id()) {
/*
* Bypass the queue if the method is called the main thread.
* Ensures that events emitted from the main thread are processed synchronously and saves a
* few CPU cycles on locking/queuing.
* As a downside, this makes main thread events prioritized over the other threads and
* disrupts chronological order.
*/
signal_t::emit(std::forward<EmitArgs>(args)...);
} else {
{
std::unique_lock lock(mutex_);
queue_.emplace(std::forward<EmitArgs>(args)...);
}
dp_.emit();
}
}
template <typename... EmitArgs>
inline void operator()(EmitArgs&&... args) {
emit(std::forward<EmitArgs>(args)...);
}
protected:
using signal_t = sigc::signal<void(std::decay_t<Args>...)>;
using slot_t = decltype(std::declval<signal_t>().make_slot());
using arg_tuple_t = std::tuple<std::decay_t<Args>...>;
// ensure that unwrapped methods are not accessible
using signal_t::emit_reverse;
using signal_t::make_slot;
void handle_event() {
for (std::unique_lock lock(mutex_); !queue_.empty(); lock.lock()) {
auto args = queue_.front();
queue_.pop();
lock.unlock();
std::apply(cached_fn_, args);
}
}
Glib::Dispatcher dp_;
std::mutex mutex_;
std::queue<arg_tuple_t> queue_;
const std::thread::id main_tid_ = std::this_thread::get_id();
// cache functor for signal emission to avoid recreating it on each event
const slot_t cached_fn_ = make_slot();
};
} // namespace waybar

View File

@ -120,7 +120,7 @@ The two arguments are:
# CUSTOM FORMATS # CUSTOM FORMATS
The *battery* module allows to define custom formats based on up to two factors. The best fitting format will be selected. The *battery* module allows one to define custom formats based on up to two factors. The best fitting format will be selected.
*format-<state>*: With *states*, a custom format can be set depending on the capacity of your battery. *format-<state>*: With *states*, a custom format can be set depending on the capacity of your battery.

View File

@ -0,0 +1,92 @@
waybar-inhibitor(5)
# NAME
waybar - inhibitor module
# DESCRIPTION
The *inhibitor* module allows to take an inhibitor lock that logind provides.
See *systemd-inhibit*(1) for more information.
# CONFIGURATION
*what*: ++
typeof: string or array ++
The inhibitor lock or locks that should be taken when active. The available inhibitor locks are *idle*, *shutdown*, *sleep*, *handle-power-key*, *handle-suspend-key*, *handle-hibernate-key* and *handle-lid-switch*.
*format*: ++
typeof: string ++
The format, how the state should be displayed.
*format-icons*: ++
typeof: array ++
Based on the current state, the corresponding icon gets selected.
*rotate*: ++
typeof: integer ++
Positive value to rotate the text label.
*max-length*: ++
typeof: integer ++
The maximum length in character the module should display.
*min-length*: ++
typeof: integer ++
The minimum length in characters the module should take up.
*align*: ++
typeof: float ++
The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text.
*on-click*: ++
typeof: string ++
Command to execute when clicked on the module. A click also toggles the state
*on-click-middle*: ++
typeof: string ++
Command to execute when middle-clicked on the module using mousewheel.
*on-click-right*: ++
typeof: string ++
Command to execute when you right clicked on the module.
*on-update*: ++
typeof: string ++
Command to execute when the module is updated.
*on-scroll-up*: ++
typeof: string ++
Command to execute when scrolling up on the module.
*on-scroll-down*: ++
typeof: string ++
Command to execute when scrolling down on the module.
*smooth-scrolling-threshold*: ++
typeof: double ++
Threshold to be used when scrolling.
*tooltip*: ++
typeof: bool ++
default: true ++
Option to disable tooltip on hover.
# FORMAT REPLACEMENTS
*{status}*: status (*activated* or *deactivated*)
*{icon}*: Icon, as defined in *format-icons*
# EXAMPLES
```
"inhibitor": {
"what": "handle-lid-switch",
"format": "{icon}",
"format-icons": {
"activated": "",
"deactivated": ""
}
}
```

View File

@ -21,6 +21,11 @@ Addressed by *river/tags*
typeof: array ++ typeof: array ++
The label to display for each tag. The label to display for each tag.
*disable-click*: ++
typeof: bool ++
default: false ++
If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled.
# EXAMPLE # EXAMPLE
``` ```

View File

@ -37,6 +37,8 @@ Addressed by *sway/language*
*{variant}*: Variant of layout (e.g. "dvorak"). *{variant}*: Variant of layout (e.g. "dvorak").
*{flag}*: Country flag of layout.
# EXAMPLES # EXAMPLES
``` ```

View File

@ -69,10 +69,6 @@ Addressed by *sway/workspaces*
typeof: string ++ typeof: string ++
Command to execute when the module is updated. Command to execute when the module is updated.
*numeric-first*: ++
typeof: bool ++
Whether to put workspaces starting with numbers before workspaces that do not start with a number.
*disable-auto-back-and-forth*: ++ *disable-auto-back-and-forth*: ++
typeof: bool ++ typeof: bool ++
Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration.
@ -120,7 +116,6 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge
"sway/workspaces": { "sway/workspaces": {
"disable-scroll": true, "disable-scroll": true,
"all-outputs": true, "all-outputs": true,
"numeric-first": false,
"format": "{name}: {icon}", "format": "{name}: {icon}",
"format-icons": { "format-icons": {
"1": "", "1": "",

View File

@ -70,12 +70,18 @@ Addressed by *wlr/taskbar*
*ignore-list*: ++ *ignore-list*: ++
typeof: array ++ typeof: array ++
List of app_id to be invisible. List of app_id/titles to be invisible.
*app_ids-mapping*: ++
typeof: object ++
Dictionary of app_id to be replaced with
# FORMAT REPLACEMENTS # FORMAT REPLACEMENTS
*{icon}*: The icon of the application. *{icon}*: The icon of the application.
*{title}*: The application name as in desktop file if appropriate desktop fils found, otherwise same as {app_id}
*{title}*: The title of the application. *{title}*: The title of the application.
*{app_id}*: The app_id (== application name) of the application. *{app_id}*: The app_id (== application name) of the application.
@ -105,7 +111,10 @@ Addressed by *wlr/taskbar*
"on-click-middle": "close", "on-click-middle": "close",
"ignore-list": [ "ignore-list": [
"Alacritty" "Alacritty"
] ],
"app_ids-mapping": {
"firefoxdeveloperedition": "firefox-developer-edition"
}
} }
``` ```

View File

@ -0,0 +1,87 @@
waybar-wlr-workspaces(5)
# NAME
waybar - wlr workspaces module
# DESCRIPTION
The *workspaces* module displays the currently used workspaces in wayland compositor.
# CONFIGURATION
Addressed by *wlr/workspaces*
*format*: ++
typeof: string ++
default: {name} ++
The format, how information should be displayed.
*format-icons*: ++
typeof: array ++
Based on the workspace name and state, the corresponding icon gets selected. See *icons*.
*sort-by-name*: ++
typeof: bool ++
default: true ++
Should workspaces be sorted by name.
*sort-by-coordinates*: ++
typeof: bool ++
default: true ++
Should workspaces be sorted by coordinates.
Note that if both *sort-by-name* and *sort-by-coordinates* are true sort by name will be first.
If both are false - sort by id will be performed.
*all-outputs*: ++
typeof: bool ++
default: false ++
If set to false workspaces group will be shown only in assigned output. Otherwise all workspace groups are shown.
*active-only*: ++
typeof: bool ++
default: false ++
If set to true only active or urgent workspaces will be shown.
# FORMAT REPLACEMENTS
*{name}*: Name of workspace assigned by compositor
*{icon}*: Icon, as defined in *format-icons*.
# CLICK ACTIONS
*activate*: Switch to workspace.
*close*: Close the workspace.
# ICONS
Additional to workspace name matching, the following *format-icons* can be set.
- *default*: Will be shown, when no string match is found.
- *focused*: Will be shown, when workspace is focused
# EXAMPLES
```
"wlr/workspaces": {
"format": "{name}: {icon}",
"format-icons": {
"1": "",
"2": "",
"3": "",
"4": "",
"5": "",
"focused": "",
"default": ""
}
}
```
# Style
- *#workspaces*
- *#workspaces button*
- *#workspaces button.active*
- *#workspaces button.urgent*
- *#workspaces button.hidden*

View File

@ -72,14 +72,25 @@ Also a minimal example configuration can be found on the at the bottom of this m
typeof: string ++ typeof: string ++
Optional name added as a CSS class, for styling multiple waybars. Optional name added as a CSS class, for styling multiple waybars.
*mode* ++
typeof: string ++
Selects one of the preconfigured display modes. This is an equivalent of the sway-bar(5) *mode* command and supports the same values: *dock*, *hide*, *invisible*, *overlay*. ++
Note: *hide* and *invisible* modes may be not as useful without Sway IPC.
*exclusive* ++ *exclusive* ++
typeof: bool ++ typeof: bool ++
default: *true* unless the layer is set to *overlay* ++ default: *true* ++
Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar. Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar.
*fixed-center* ++
typeof: bool ++
default: *true*
Prefer fixed center position for the `modules-center` block. The center block will stay in the middle of the bar whenever possible. It can still be pushed around if other blocks need more space.
When false, the center block is centered in the space between the left and right block.
*passthrough* ++ *passthrough* ++
typeof: bool ++ typeof: bool ++
default: *false* unless the layer is set to *overlay* ++ default: *false* ++
Option to pass any pointer events to the window under the bar. Option to pass any pointer events to the window under the bar.
Intended to be used with either *top* or *overlay* layers and without exclusive zone. Intended to be used with either *top* or *overlay* layers and without exclusive zone.
@ -89,6 +100,16 @@ Also a minimal example configuration can be found on the at the bottom of this m
Option to disable the use of gtk-layer-shell for popups. Option to disable the use of gtk-layer-shell for popups.
Only functional if compiled with gtk-layer-shell support. Only functional if compiled with gtk-layer-shell support.
*ipc* ++
typeof: bool ++
default: false ++
Option to subscribe to the Sway IPC bar configuration and visibility events and control waybar with *swaymsg bar* commands. ++
Requires *bar_id* value from sway configuration to be either passed with the *-b* commandline argument or specified with the *id* option.
*id* ++
typeof: string ++
*bar_id* for the Sway IPC. Use this if you need to override the value passed with the *-b bar_id* commandline argument for the specific bar instance.
*include* ++ *include* ++
typeof: string|array ++ typeof: string|array ++
Paths to additional configuration files. Paths to additional configuration files.
@ -203,6 +224,28 @@ When positioning Waybar on the left or right side of the screen, sometimes it's
Valid options for the "rotate" property are: 0, 90, 180 and 270. Valid options for the "rotate" property are: 0, 90, 180 and 270.
## Grouping modules
Module groups allow stacking modules in the direction orthogonal to the bar direction. When the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally.
A module group is defined by specifying a module named "group/some-group-name". The group must also be configured with a list of contained modules. Example:
```
{
"modules-right": ["group/hardware", "clock"],
"group/hardware": {
"modules": [
"cpu",
"memory",
"battery"
]
},
...
}
```
# SUPPORTED MODULES # SUPPORTED MODULES
- *waybar-backlight(5)* - *waybar-backlight(5)*
@ -224,5 +267,6 @@ Valid options for the "rotate" property are: 0, 90, 180 and 270.
- *waybar-sway-window(5)* - *waybar-sway-window(5)*
- *waybar-sway-workspaces(5)* - *waybar-sway-workspaces(5)*
- *waybar-wlr-taskbar(5)* - *waybar-wlr-taskbar(5)*
- *waybar-wlr-workspaces(5)*
- *waybar-temperature(5)* - *waybar-temperature(5)*
- *waybar-tray(5)* - *waybar-tray(5)*

View File

@ -86,7 +86,7 @@ wayland_cursor = dependency('wayland-cursor')
wayland_protos = dependency('wayland-protocols') wayland_protos = dependency('wayland-protocols')
gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0'])
dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk'))
giounix = dependency('gio-unix-2.0', required: get_option('dbusmenu-gtk')) giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled()))
jsoncpp = dependency('jsoncpp') jsoncpp = dependency('jsoncpp')
sigcpp = dependency('sigc++-2.0') sigcpp = dependency('sigc++-2.0')
libepoll = dependency('epoll-shim', required: false) libepoll = dependency('epoll-shim', required: false)
@ -150,6 +150,7 @@ src_files = files(
'src/bar.cpp', 'src/bar.cpp',
'src/client.cpp', 'src/client.cpp',
'src/config.cpp', 'src/config.cpp',
'src/group.cpp',
'src/util/ustring_clen.cpp' 'src/util/ustring_clen.cpp'
) )
@ -177,6 +178,7 @@ endif
add_project_arguments('-DHAVE_SWAY', language: 'cpp') add_project_arguments('-DHAVE_SWAY', language: 'cpp')
src_files += [ src_files += [
'src/modules/sway/ipc/client.cpp', 'src/modules/sway/ipc/client.cpp',
'src/modules/sway/bar.cpp',
'src/modules/sway/mode.cpp', 'src/modules/sway/mode.cpp',
'src/modules/sway/language.cpp', 'src/modules/sway/language.cpp',
'src/modules/sway/window.cpp', 'src/modules/sway/window.cpp',
@ -186,6 +188,8 @@ src_files += [
if true if true
add_project_arguments('-DHAVE_WLR', language: 'cpp') add_project_arguments('-DHAVE_WLR', language: 'cpp')
src_files += 'src/modules/wlr/taskbar.cpp' src_files += 'src/modules/wlr/taskbar.cpp'
src_files += 'src/modules/wlr/workspace_manager.cpp'
src_files += 'src/modules/wlr/workspace_manager_binding.cpp'
endif endif
if true if true
@ -238,6 +242,11 @@ if libsndio.found()
src_files += 'src/modules/sndio.cpp' src_files += 'src/modules/sndio.cpp'
endif endif
if (giounix.found() and not get_option('logind').disabled())
add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp')
src_files += 'src/modules/inhibitor.cpp'
endif
if get_option('rfkill').enabled() if get_option('rfkill').enabled()
if is_linux if is_linux
add_project_arguments('-DWANT_RFKILL', language: 'cpp') add_project_arguments('-DWANT_RFKILL', language: 'cpp')
@ -255,6 +264,10 @@ else
src_files += 'src/modules/simpleclock.cpp' src_files += 'src/modules/simpleclock.cpp'
endif endif
if get_option('experimental')
add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp')
endif
subdir('protocol') subdir('protocol')
executable( executable(
@ -282,7 +295,7 @@ executable(
gtk_layer_shell, gtk_layer_shell,
libsndio, libsndio,
tz_dep, tz_dep,
xkbregistry xkbregistry
], ],
include_directories: [include_directories('include')], include_directories: [include_directories('include')],
install: true, install: true,
@ -334,10 +347,15 @@ if scdoc.found()
'waybar-tray.5.scd', 'waybar-tray.5.scd',
'waybar-states.5.scd', 'waybar-states.5.scd',
'waybar-wlr-taskbar.5.scd', 'waybar-wlr-taskbar.5.scd',
'waybar-wlr-workspaces.5.scd',
'waybar-bluetooth.5.scd', 'waybar-bluetooth.5.scd',
'waybar-sndio.5.scd', 'waybar-sndio.5.scd',
] ]
if (giounix.found() and not get_option('logind').disabled())
man_files += 'waybar-inhibitor.5.scd'
endif
foreach file : man_files foreach file : man_files
path = '@0@'.format(file) path = '@0@'.format(file)
basename = path.split('/')[-1] basename = path.split('/')[-1]
@ -380,3 +398,4 @@ if clangtidy.found()
'-p', meson.build_root() '-p', meson.build_root()
] + src_files) ] + src_files)
endif endif

View File

@ -10,4 +10,6 @@ option('mpd', type: 'feature', value: 'auto', description: 'Enable support for t
option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support') option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support')
option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL')
option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio')
option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind')
option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('tests', type: 'feature', value: 'auto', description: 'Enable tests')
option('experimental', type : 'boolean', value : false, description: 'Enable experimental features')

View File

@ -0,0 +1,306 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="ext_workspace_unstable_v1">
<copyright>
Copyright © 2019 Christopher Billington
Copyright © 2020 Ilia Bozhinov
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zext_workspace_manager_v1" version="1">
<description summary="list and control workspaces">
Workspaces, also called virtual desktops, are groups of surfaces. A
compositor with a concept of workspaces may only show some such groups of
surfaces (those of 'active' workspaces) at a time. 'Activating' a
workspace is a request for the compositor to display that workspace's
surfaces as normal, whereas the compositor may hide or otherwise
de-emphasise surfaces that are associated only with 'inactive' workspaces.
Workspaces are grouped by which sets of outputs they correspond to, and
may contain surfaces only from those outputs. In this way, it is possible
for each output to have its own set of workspaces, or for all outputs (or
any other arbitrary grouping) to share workspaces. Compositors may
optionally conceptually arrange each group of workspaces in an
N-dimensional grid.
The purpose of this protocol is to enable the creation of taskbars and
docks by providing them with a list of workspaces and their properties,
and allowing them to activate and deactivate workspaces.
After a client binds the zext_workspace_manager_v1, each workspace will be
sent via the workspace event.
</description>
<event name="workspace_group">
<description summary="a workspace group has been created">
This event is emitted whenever a new workspace group has been created.
All initial details of the workspace group (workspaces, outputs) will be
sent immediately after this event via the corresponding events in
zext_workspace_group_handle_v1.
</description>
<arg name="workspace_group" type="new_id" interface="zext_workspace_group_handle_v1"/>
</event>
<request name="commit">
<description summary="all requests about the workspaces have been sent">
The client must send this request after it has finished sending other
requests. The compositor must process a series of requests preceding a
commit request atomically.
This allows changes to the workspace properties to be seen as atomic,
even if they happen via multiple events, and even if they involve
multiple zext_workspace_handle_v1 objects, for example, deactivating one
workspace and activating another.
</description>
</request>
<event name="done">
<description summary="all information about the workspace groups has been sent">
This event is sent after all changes in all workspace groups have been
sent.
This allows changes to one or more zext_workspace_group_handle_v1
properties to be seen as atomic, even if they happen via multiple
events. In particular, an output moving from one workspace group to
another sends an output_enter event and an output_leave event to the two
zext_workspace_group_handle_v1 objects in question. The compositor sends
the done event only after updating the output information in both
workspace groups.
</description>
</event>
<event name="finished">
<description summary="the compositor has finished with the workspace_manager">
This event indicates that the compositor is done sending events to the
zext_workspace_manager_v1. The server will destroy the object
immediately after sending this request, so it will become invalid and
the client should free any resources associated with it.
</description>
</event>
<request name="stop">
<description summary="stop sending events">
Indicates the client no longer wishes to receive events for new
workspace groups. However the compositor may emit further workspace
events, until the finished event is emitted.
The client must not send any more requests after this one.
</description>
</request>
</interface>
<interface name="zext_workspace_group_handle_v1" version="1">
<description summary="a workspace group assigned to a set of outputs">
A zext_workspace_group_handle_v1 object represents a a workspace group
that is assigned a set of outputs and contains a number of workspaces.
The set of outputs assigned to the workspace group is conveyed to the client via
output_enter and output_leave events, and its workspaces are conveyed with
workspace events.
For example, a compositor which has a set of workspaces for each output may
advertise a workspace group (and its workspaces) per output, whereas a compositor
where a workspace spans all outputs may advertise a single workspace group for all
outputs.
</description>
<event name="output_enter">
<description summary="output assigned to workspace group">
This event is emitted whenever an output is assigned to the workspace
group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="output_leave">
<description summary="output removed from workspace group">
This event is emitted whenever an output is removed from the workspace
group.
</description>
<arg name="output" type="object" interface="wl_output"/>
</event>
<event name="workspace">
<description summary="workspace added to workspace group">
This event is emitted whenever a new workspace has been created.
All initial details of the workspace (name, coordinates, state) will
be sent immediately after this event via the corresponding events in
zext_workspace_handle_v1.
</description>
<arg name="workspace" type="new_id" interface="zext_workspace_handle_v1"/>
</event>
<event name="remove">
<description summary="this workspace group has been destroyed">
This event means the zext_workspace_group_handle_v1 has been destroyed.
It is guaranteed there won't be any more events for this
zext_workspace_group_handle_v1. The zext_workspace_group_handle_v1 becomes
inert so any requests will be ignored except the destroy request.
The compositor must remove all workspaces belonging to a workspace group
before removing the workspace group.
</description>
</event>
<request name="create_workspace">
<description summary="create a new workspace">
Request that the compositor create a new workspace with the given name.
There is no guarantee that the compositor will create a new workspace,
or that the created workspace will have the provided name.
</description>
<arg name="workspace" type="string"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the zext_workspace_handle_v1 object">
Destroys the zext_workspace_handle_v1 object.
This request should be called either when the client does not want to
use the workspace object any more or after the remove event to finalize
the destruction of the object.
</description>
</request>
</interface>
<interface name="zext_workspace_handle_v1" version="1">
<description summary="a workspace handing a group of surfaces">
A zext_workspace_handle_v1 object represents a a workspace that handles a
group of surfaces.
Each workspace has a name, conveyed to the client with the name event; a
list of states, conveyed to the client with the state event; and
optionally a set of coordinates, conveyed to the client with the
coordinates event. The client may request that the compositor activate or
deactivate the workspace.
Each workspace can belong to only a single workspace group.
Depepending on the compositor policy, there might be workspaces with
the same name in different workspace groups, but these workspaces are still
separate (e.g. one of them might be active while the other is not).
</description>
<event name="name">
<description summary="workspace name changed">
This event is emitted immediately after the zext_workspace_handle_v1 is
created and whenever the name of the workspace changes.
</description>
<arg name="name" type="string"/>
</event>
<event name="coordinates">
<description summary="workspace coordinates changed">
This event is used to organize workspaces into an N-dimensional grid
within a workspace group, and if supported, is emitted immediately after
the zext_workspace_handle_v1 is created and whenever the coordinates of
the workspace change. Compositors may not send this event if they do not
conceptually arrange workspaces in this way. If compositors simply
number workspaces, without any geometric interpretation, they may send
1D coordinates, which clients should not interpret as implying any
geometry. Sending an empty array means that the compositor no longer
orders the workspace geometrically.
Coordinates have an arbitrary number of dimensions N with an uint32
position along each dimension. By convention if N > 1, the first
dimension is X, the second Y, the third Z, and so on. The compositor may
chose to utilize these events for a more novel workspace layout
convention, however. No guarantee is made about the grid being filled or
bounded; there may be a workspace at coordinate 1 and another at
coordinate 1000 and none in between. Within a workspace group, however,
workspaces must have unique coordinates of equal dimensionality.
</description>
<arg name="coordinates" type="array"/>
</event>
<event name="state">
<description summary="the state of the workspace changed">
This event is emitted immediately after the zext_workspace_handle_v1 is
created and each time the workspace state changes, either because of a
compositor action or because of a request in this protocol.
</description>
<arg name="state" type="array"/>
</event>
<enum name="state">
<description summary="types of states on the workspace">
The different states that a workspace can have.
</description>
<entry name="active" value="0" summary="the workspace is active"/>
<entry name="urgent" value="1" summary="the workspace requests attention"/>
<entry name="hidden" value="2">
<description summary="the workspace is not visible">
The workspace is not visible in its workspace group, and clients
attempting to visualize the compositor workspace state should not
display such workspaces.
</description>
</entry>
</enum>
<event name="remove">
<description summary="this workspace has been destroyed">
This event means the zext_workspace_handle_v1 has been destroyed. It is
guaranteed there won't be any more events for this
zext_workspace_handle_v1. The zext_workspace_handle_v1 becomes inert so
any requests will be ignored except the destroy request.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the zext_workspace_handle_v1 object">
Destroys the zext_workspace_handle_v1 object.
This request should be called either when the client does not want to
use the workspace object any more or after the remove event to finalize
the destruction of the object.
</description>
</request>
<request name="activate">
<description summary="activate the workspace">
Request that this workspace be activated.
There is no guarantee the workspace will be actually activated, and
behaviour may be compositor-dependent. For example, activating a
workspace may or may not deactivate all other workspaces in the same
group.
</description>
</request>
<request name="deactivate">
<description summary="activate the workspace">
Request that this workspace be deactivated.
There is no guarantee the workspace will be actually deactivated.
</description>
</request>
<request name="remove">
<description summary="remove the workspace">
Request that this workspace be removed.
There is no guarantee the workspace will be actually removed.
</description>
</request>
</interface>
</protocol>

View File

@ -27,7 +27,9 @@ client_protocols = [
[wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'], [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'],
['wlr-layer-shell-unstable-v1.xml'], ['wlr-layer-shell-unstable-v1.xml'],
['wlr-foreign-toplevel-management-unstable-v1.xml'], ['wlr-foreign-toplevel-management-unstable-v1.xml'],
['ext-workspace-unstable-v1.xml'],
['river-status-unstable-v1.xml'], ['river-status-unstable-v1.xml'],
['river-control-unstable-v1.xml'],
] ]
client_protos_src = [] client_protos_src = []

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="river_control_unstable_v1">
<copyright>
Copyright 2020 The River Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</copyright>
<interface name="zriver_control_v1" version="1">
<description summary="run compositor commands">
This interface allows clients to run compositor commands and receive a
success/failure response with output or a failure message respectively.
Each command is built up in a series of add_argument requests and
executed with a run_command request. The first argument is the command
to be run.
A complete list of commands should be made available in the man page of
the compositor.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the river_control object">
This request indicates that the client will not use the
river_control object any more. Objects that have been created
through this instance are not affected.
</description>
</request>
<request name="add_argument">
<description summary="add an argument to the current command">
Arguments are stored by the server in the order they were sent until
the run_command request is made.
</description>
<arg name="argument" type="string" summary="the argument to add"/>
</request>
<request name="run_command">
<description summary="run the current command">
Execute the command built up using the add_argument request for the
given seat.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="callback" type="new_id" interface="zriver_command_callback_v1"
summary="callback object"/>
</request>
</interface>
<interface name="zriver_command_callback_v1" version="1">
<description summary="callback object">
This object is created by the run_command request. Exactly one of the
success or failure events will be sent. This object will be destroyed
by the compositor after one of the events is sent.
</description>
<event name="success">
<description summary="command successful">
Sent when the command has been successfully received and executed by
the compositor. Some commands may produce output, in which case the
output argument will be a non-empty string.
</description>
<arg name="output" type="string" summary="the output of the command"/>
</event>
<event name="failure">
<description summary="command failed">
Sent when the command could not be carried out. This could be due to
sending a non-existent command, no command, not enough arguments, too
many arguments, invalid arguments, etc.
</description>
<arg name="failure_message" type="string"
summary="a message explaining why failure occurred"/>
</event>
</interface>
</protocol>

View File

@ -110,6 +110,7 @@ def main():
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
for player in manager.props.player_names: for player in manager.props.player_names:
if arguments.player is not None and arguments.player != player.name: if arguments.player is not None and arguments.player != player.name:

View File

@ -9,8 +9,13 @@
#include "bar.hpp" #include "bar.hpp"
#include "client.hpp" #include "client.hpp"
#include "factory.hpp" #include "factory.hpp"
#include "group.hpp"
#include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h"
#ifdef HAVE_SWAY
#include "modules/sway/bar.hpp"
#endif
namespace waybar { namespace waybar {
static constexpr const char* MIN_HEIGHT_MSG = static constexpr const char* MIN_HEIGHT_MSG =
"Requested height: {} is less than the minimum height: {} required by the modules"; "Requested height: {} is less than the minimum height: {} required by the modules";
@ -23,6 +28,84 @@ static constexpr const char* BAR_SIZE_MSG = "Bar configured (width: {}, height:
static constexpr const char* SIZE_DEFINED = static constexpr const char* SIZE_DEFINED =
"{} size is defined in the config file so it will stay like that"; "{} size is defined in the config file so it will stay like that";
const Bar::bar_mode_map Bar::PRESET_MODES = { //
{"default",
{// Special mode to hold the global bar configuration
.layer = bar_layer::BOTTOM,
.exclusive = true,
.passthrough = false,
.visible = true}},
{"dock",
{// Modes supported by the sway config; see man sway-bar(5)
.layer = bar_layer::BOTTOM,
.exclusive = true,
.passthrough = false,
.visible = true}},
{"hide",
{//
.layer = bar_layer::TOP,
.exclusive = false,
.passthrough = false,
.visible = true}},
{"invisible",
{//
.layer = bar_layer::BOTTOM,
.exclusive = false,
.passthrough = true,
.visible = false}},
{"overlay",
{//
.layer = bar_layer::TOP,
.exclusive = false,
.passthrough = true,
.visible = true}}};
const std::string_view Bar::MODE_DEFAULT = "default";
const std::string_view Bar::MODE_INVISIBLE = "invisible";
const std::string_view DEFAULT_BAR_ID = "bar-0";
/* Deserializer for enum bar_layer */
void from_json(const Json::Value& j, bar_layer& l) {
if (j == "bottom") {
l = bar_layer::BOTTOM;
} else if (j == "top") {
l = bar_layer::TOP;
} else if (j == "overlay") {
l = bar_layer::OVERLAY;
}
}
/* Deserializer for struct bar_mode */
void from_json(const Json::Value& j, bar_mode& m) {
if (j.isObject()) {
if (auto v = j["layer"]; v.isString()) {
from_json(v, m.layer);
}
if (auto v = j["exclusive"]; v.isBool()) {
m.exclusive = v.asBool();
}
if (auto v = j["passthrough"]; v.isBool()) {
m.passthrough = v.asBool();
}
if (auto v = j["visible"]; v.isBool()) {
m.visible = v.asBool();
}
}
}
/* Deserializer for JSON Object -> map<string compatible type, Value>
* Assumes that all the values in the object are deserializable to the same type.
*/
template <typename Key, typename Value,
typename = std::enable_if_t<std::is_convertible<std::string_view, Key>::value>>
void from_json(const Json::Value& j, std::map<Key, Value>& m) {
if (j.isObject()) {
for (auto it = j.begin(); it != j.end(); ++it) {
from_json(*it, m[it.key().asString()]);
}
}
}
#ifdef HAVE_GTK_LAYER_SHELL #ifdef HAVE_GTK_LAYER_SHELL
struct GLSSurfaceImpl : public BarSurface, public sigc::trackable { struct GLSSurfaceImpl : public BarSurface, public sigc::trackable {
GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} {
@ -391,7 +474,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
: output(w_output), : output(w_output),
config(w_config), config(w_config),
window{Gtk::WindowType::WINDOW_TOPLEVEL}, window{Gtk::WindowType::WINDOW_TOPLEVEL},
layer_{bar_layer::BOTTOM},
left_(Gtk::ORIENTATION_HORIZONTAL, 0), left_(Gtk::ORIENTATION_HORIZONTAL, 0),
center_(Gtk::ORIENTATION_HORIZONTAL, 0), center_(Gtk::ORIENTATION_HORIZONTAL, 0),
right_(Gtk::ORIENTATION_HORIZONTAL, 0), right_(Gtk::ORIENTATION_HORIZONTAL, 0),
@ -403,27 +485,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
window.get_style_context()->add_class(config["name"].asString()); window.get_style_context()->add_class(config["name"].asString());
window.get_style_context()->add_class(config["position"].asString()); window.get_style_context()->add_class(config["position"].asString());
if (config["layer"] == "top") {
layer_ = bar_layer::TOP;
} else if (config["layer"] == "overlay") {
layer_ = bar_layer::OVERLAY;
}
if (config["exclusive"].isBool()) {
exclusive = config["exclusive"].asBool();
} else if (layer_ == bar_layer::OVERLAY) {
// swaybar defaults: overlay mode does not reserve an exclusive zone
exclusive = false;
}
bool passthrough = false;
if (config["passthrough"].isBool()) {
passthrough = config["passthrough"].asBool();
} else if (layer_ == bar_layer::OVERLAY) {
// swaybar defaults: overlay mode does not accept pointer events.
passthrough = true;
}
auto position = config["position"].asString(); auto position = config["position"].asString();
if (position == "right" || position == "left") { if (position == "right" || position == "left") {
@ -505,15 +566,43 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
surface_impl_ = std::make_unique<RawSurfaceImpl>(window, *output); surface_impl_ = std::make_unique<RawSurfaceImpl>(window, *output);
} }
surface_impl_->setLayer(layer_);
surface_impl_->setExclusiveZone(exclusive);
surface_impl_->setMargins(margins_); surface_impl_->setMargins(margins_);
surface_impl_->setPassThrough(passthrough);
surface_impl_->setPosition(position); surface_impl_->setPosition(position);
surface_impl_->setSize(width, height); surface_impl_->setSize(width, height);
/* Read custom modes if available */
if (auto modes = config.get("modes", {}); modes.isObject()) {
from_json(modes, configured_modes);
}
/* Update "default" mode with the global bar options */
from_json(config, configured_modes[MODE_DEFAULT]);
if (auto mode = config.get("mode", {}); mode.isString()) {
setMode(config["mode"].asString());
} else {
setMode(MODE_DEFAULT);
}
window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap));
#if HAVE_SWAY
if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) {
bar_id = Client::inst()->bar_id;
if (auto id = config["id"]; id.isString()) {
bar_id = id.asString();
}
if (bar_id.empty()) {
bar_id = DEFAULT_BAR_ID;
}
try {
_ipc_client = std::make_unique<BarIpcClient>(*this);
} catch (const std::exception& exc) {
spdlog::warn("Failed to open bar ipc connection: {}", exc.what());
}
}
#endif
setupWidgets(); setupWidgets();
window.show_all(); window.show_all();
@ -528,6 +617,44 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
} }
} }
/* Need to define it here because of forward declared members */
waybar::Bar::~Bar() = default;
void waybar::Bar::setMode(const std::string_view& mode) {
using namespace std::literals::string_literals;
auto style = window.get_style_context();
/* remove styles added by previous setMode calls */
style->remove_class("mode-"s + last_mode_);
auto it = configured_modes.find(mode);
if (it != configured_modes.end()) {
last_mode_ = mode;
style->add_class("mode-"s + last_mode_);
setMode(it->second);
} else {
spdlog::warn("Unknown mode \"{}\" requested", mode);
last_mode_ = MODE_DEFAULT;
style->add_class("mode-"s + last_mode_);
setMode(configured_modes.at(MODE_DEFAULT));
}
}
void waybar::Bar::setMode(const struct bar_mode& mode) {
surface_impl_->setLayer(mode.layer);
surface_impl_->setExclusiveZone(mode.exclusive);
surface_impl_->setPassThrough(mode.passthrough);
if (mode.visible) {
window.get_style_context()->remove_class("hidden");
window.set_opacity(1);
} else {
window.get_style_context()->add_class("hidden");
window.set_opacity(0);
}
surface_impl_->commit();
}
void waybar::Bar::onMap(GdkEventAny*) { void waybar::Bar::onMap(GdkEventAny*) {
/* /*
* Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor).
@ -538,17 +665,7 @@ void waybar::Bar::onMap(GdkEventAny*) {
void waybar::Bar::setVisible(bool value) { void waybar::Bar::setVisible(bool value) {
visible = value; visible = value;
if (!visible) { setMode(visible ? MODE_DEFAULT : MODE_INVISIBLE);
window.get_style_context()->add_class("hidden");
window.set_opacity(0);
surface_impl_->setLayer(bar_layer::BOTTOM);
} else {
window.get_style_context()->remove_class("hidden");
window.set_opacity(1);
surface_impl_->setLayer(layer_);
}
surface_impl_->setExclusiveZone(exclusive && visible);
surface_impl_->commit();
} }
void waybar::Bar::toggle() { setVisible(!visible); } void waybar::Bar::toggle() { setVisible(!visible); }
@ -594,19 +711,7 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) {
} }
void waybar::Bar::handleSignal(int signal) { void waybar::Bar::handleSignal(int signal) {
for (auto& module : modules_left_) { for (auto& module : modules_all_) {
auto* custom = dynamic_cast<waybar::modules::Custom*>(module.get());
if (custom != nullptr) {
custom->refresh(signal);
}
}
for (auto& module : modules_center_) {
auto* custom = dynamic_cast<waybar::modules::Custom*>(module.get());
if (custom != nullptr) {
custom->refresh(signal);
}
}
for (auto& module : modules_right_) {
auto* custom = dynamic_cast<waybar::modules::Custom*>(module.get()); auto* custom = dynamic_cast<waybar::modules::Custom*>(module.get());
if (custom != nullptr) { if (custom != nullptr) {
custom->refresh(signal); custom->refresh(signal);
@ -614,19 +719,36 @@ void waybar::Bar::handleSignal(int signal) {
} }
} }
void waybar::Bar::getModules(const Factory& factory, const std::string& pos) { void waybar::Bar::getModules(const Factory& factory, const std::string& pos, Gtk::Box* group = nullptr) {
if (config[pos].isArray()) { auto module_list = group ? config[pos]["modules"] : config[pos];
for (const auto& name : config[pos]) { if (module_list.isArray()) {
for (const auto& name : module_list) {
try { try {
auto module = factory.makeModule(name.asString()); auto ref = name.asString();
if (pos == "modules-left") { AModule* module;
modules_left_.emplace_back(module);
if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) {
auto group_module = new waybar::Group(ref, *this, config[ref]);
getModules(factory, ref, &group_module->box);
module = group_module;
} else {
module = factory.makeModule(ref);
} }
if (pos == "modules-center") {
modules_center_.emplace_back(module); std::shared_ptr<AModule> module_sp(module);
} modules_all_.emplace_back(module_sp);
if (pos == "modules-right") { if (group) {
modules_right_.emplace_back(module); group->pack_start(*module, false, false);
} else {
if (pos == "modules-left") {
modules_left_.emplace_back(module_sp);
}
if (pos == "modules-center") {
modules_center_.emplace_back(module_sp);
}
if (pos == "modules-right") {
modules_right_.emplace_back(module_sp);
}
} }
module->dp.connect([module, &name] { module->dp.connect([module, &name] {
try { try {
@ -645,7 +767,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos) {
auto waybar::Bar::setupWidgets() -> void { auto waybar::Bar::setupWidgets() -> void {
window.add(box_); window.add(box_);
box_.pack_start(left_, false, false); box_.pack_start(left_, false, false);
box_.set_center_widget(center_); if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) {
box_.set_center_widget(center_);
} else {
box_.pack_start(center_, true, false);
}
box_.pack_end(right_, false, false); box_.pack_end(right_, false, false);
// Convert to button code for every module that is used. // Convert to button code for every module that is used.

View File

@ -199,7 +199,6 @@ int waybar::Client::main(int argc, char *argv[]) {
bool show_version = false; bool show_version = false;
std::string config_opt; std::string config_opt;
std::string style_opt; std::string style_opt;
std::string bar_id;
std::string log_level; std::string log_level;
auto cli = clara::detail::Help(show_help) | auto cli = clara::detail::Help(show_help) |
clara::detail::Opt(show_version)["-v"]["--version"]("Show version") | clara::detail::Opt(show_version)["-v"]["--version"]("Show version") |

View File

@ -30,6 +30,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
if (ref == "wlr/taskbar") { if (ref == "wlr/taskbar") {
return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]);
} }
#ifdef USE_EXPERIMENTAL
if (ref == "wlr/workspaces") {
return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]);
}
#endif
#endif #endif
#ifdef HAVE_RIVER #ifdef HAVE_RIVER
if (ref == "river/tags") { if (ref == "river/tags") {
@ -89,6 +94,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
if (ref == "sndio") { if (ref == "sndio") {
return new waybar::modules::Sndio(id, config_[name]); return new waybar::modules::Sndio(id, config_[name]);
} }
#endif
#ifdef HAVE_GIO_UNIX
if (ref == "inhibitor") {
return new waybar::modules::Inhibitor(id, bar_, config_[name]);
}
#endif #endif
if (ref == "temperature") { if (ref == "temperature") {
return new waybar::modules::Temperature(id, config_[name]); return new waybar::modules::Temperature(id, config_[name]);

19
src/group.cpp Normal file
View File

@ -0,0 +1,19 @@
#include "group.hpp"
#include <fmt/format.h>
#include <util/command.hpp>
namespace waybar {
Group::Group(const std::string& name, const Bar& bar, const Json::Value& config)
: AModule(config, name, "", false, false),
box{bar.vertical ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL, 0}
{
}
auto Group::update() -> void {
// noop
}
Group::operator Gtk::Widget&() { return box; }
} // namespace waybar

View File

@ -14,17 +14,51 @@
using waybar::modules::waybar_time; using waybar::modules::waybar_time;
waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
: ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), fixed_time_zone_(false) { : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true),
current_time_zone_idx_(0),
is_calendar_in_tooltip_(false)
{
if (config_["timezones"].isArray() && !config_["timezones"].empty()) { if (config_["timezones"].isArray() && !config_["timezones"].empty()) {
time_zone_idx_ = 0; for (const auto& zone_name: config_["timezones"]) {
setTimeZone(config_["timezones"][time_zone_idx_]); if (!zone_name.isString() || zone_name.asString().empty()) {
} else { time_zones_.push_back(nullptr);
setTimeZone(config_["timezone"]); continue;
}
time_zones_.push_back(
date::locate_zone(
zone_name.asString()
)
);
}
} else if (config_["timezone"].isString() && !config_["timezone"].asString().empty()) {
time_zones_.push_back(
date::locate_zone(
config_["timezone"].asString()
)
);
} }
if (fixed_time_zone_) {
// If all timezones are parsed and no one is good, add nullptr to the timezones vector, to mark that local time should be shown.
if (!time_zones_.size()) {
time_zones_.push_back(nullptr);
}
if (!is_timezone_fixed()) {
spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018."); spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018.");
} }
// Check if a particular placeholder is present in the tooltip format, to know what to calculate on update.
if (config_["tooltip-format"].isString()) {
std::string trimmed_format = config_["tooltip-format"].asString();
trimmed_format.erase(std::remove_if(trimmed_format.begin(),
trimmed_format.end(),
[](unsigned char x){return std::isspace(x);}),
trimmed_format.end());
if (trimmed_format.find("{" + kCalendarPlaceholder + "}") != std::string::npos) {
is_calendar_in_tooltip_ = true;
}
}
if (config_["locale"].isString()) { if (config_["locale"].isString()) {
locale_ = std::locale(config_["locale"].asString()); locale_ = std::locale(config_["locale"].asString());
} else { } else {
@ -40,53 +74,46 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
}; };
} }
const date::time_zone* waybar::modules::Clock::current_timezone() {
return time_zones_[current_time_zone_idx_] ? time_zones_[current_time_zone_idx_] : date::current_zone();
}
bool waybar::modules::Clock::is_timezone_fixed() {
return time_zones_[current_time_zone_idx_] != nullptr;
}
auto waybar::modules::Clock::update() -> void { auto waybar::modules::Clock::update() -> void {
if (!fixed_time_zone_) { auto time_zone = current_timezone();
// Time zone can change. Be sure to pick that. auto now = std::chrono::system_clock::now();
time_zone_ = date::current_zone();
}
auto now = std::chrono::system_clock::now();
waybar_time wtime = {locale_, waybar_time wtime = {locale_,
date::make_zoned(time_zone_, date::floor<std::chrono::seconds>(now))}; date::make_zoned(time_zone, date::floor<std::chrono::seconds>(now))};
std::string text = "";
std::string text; if (!is_timezone_fixed()) {
if (!fixed_time_zone_) {
// As date dep is not fully compatible, prefer fmt // As date dep is not fully compatible, prefer fmt
tzset(); tzset();
auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now));
text = fmt::format(format_, localtime); text = fmt::format(format_, localtime);
label_.set_markup(text);
} else { } else {
text = fmt::format(format_, wtime); text = fmt::format(format_, wtime);
label_.set_markup(text);
} }
label_.set_markup(text);
if (tooltipEnabled()) { if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) { if (config_["tooltip-format"].isString()) {
const auto calendar = calendar_text(wtime); std::string calendar_lines = "";
auto tooltip_format = config_["tooltip-format"].asString(); if (is_calendar_in_tooltip_) {
auto tooltip_text = fmt::format(tooltip_format, wtime, fmt::arg("calendar", calendar)); calendar_lines = calendar_text(wtime);
label_.set_tooltip_markup(tooltip_text); }
} else { auto tooltip_format = config_["tooltip-format"].asString();
label_.set_tooltip_markup(text); text = fmt::format(tooltip_format, wtime, fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines));
} }
} }
label_.set_tooltip_markup(text);
// Call parent update // Call parent update
ALabel::update(); ALabel::update();
} }
bool waybar::modules::Clock::setTimeZone(Json::Value zone_name) {
if (!zone_name.isString() || zone_name.asString().empty()) {
fixed_time_zone_ = false;
return false;
}
time_zone_ = date::locate_zone(zone_name.asString());
fixed_time_zone_ = true;
return true;
}
bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) { bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) {
// defer to user commands if set // defer to user commands if set
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
@ -97,17 +124,18 @@ bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) {
if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) { if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) {
return true; return true;
} }
if (!config_["timezones"].isArray() || config_["timezones"].empty()) { if (time_zones_.size() == 1) {
return true; return true;
} }
auto nr_zones = config_["timezones"].size();
auto nr_zones = time_zones_.size();
if (dir == SCROLL_DIR::UP) { if (dir == SCROLL_DIR::UP) {
size_t new_idx = time_zone_idx_ + 1; size_t new_idx = current_time_zone_idx_ + 1;
time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx;
} else { } else {
time_zone_idx_ = time_zone_idx_ == 0 ? nr_zones - 1 : time_zone_idx_ - 1; current_time_zone_idx_ = current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1;
} }
setTimeZone(config_["timezones"][time_zone_idx_]);
update(); update();
return true; return true;
} }

View File

@ -45,9 +45,9 @@ auto waybar::modules::Disk::update() -> void {
} }
auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true);
auto used = pow_format((stats.f_blocks - stats.f_bavail) * stats.f_frsize, "B", true); auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true);
auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true);
auto percentage_used = (stats.f_blocks - stats.f_bavail) * 100 / stats.f_blocks; auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks;
auto format = format_; auto format = format_;
auto state = getState(percentage_used); auto state = getState(percentage_used);

175
src/modules/inhibitor.cpp Normal file
View File

@ -0,0 +1,175 @@
#include "modules/inhibitor.hpp"
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <spdlog/spdlog.h>
namespace {
using DBus = std::unique_ptr<GDBusConnection, void(*)(GDBusConnection*)>;
auto dbus() -> DBus {
GError *error = nullptr;
GDBusConnection* connection =
g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
if (error) {
spdlog::error("g_bus_get_sync() failed: {}", error->message);
g_error_free(error);
connection = nullptr;
}
auto destructor = [](GDBusConnection* connection) {
GError *error = nullptr;
g_dbus_connection_close_sync(connection, nullptr, &error);
if (error) {
spdlog::error(
"g_bus_connection_close_sync failed(): {}",
error->message);
g_error_free(error);
}
};
return DBus{connection, destructor};
}
auto getLocks(const DBus& bus, const std::string& inhibitors) -> int {
GError *error = nullptr;
GUnixFDList* fd_list;
int handle;
auto reply = g_dbus_connection_call_with_unix_fd_list_sync(bus.get(),
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"Inhibit",
g_variant_new(
"(ssss)",
inhibitors.c_str(),
"waybar",
"Asked by user",
"block"),
G_VARIANT_TYPE("(h)"),
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&fd_list,
nullptr,
&error);
if (error) {
spdlog::error(
"g_dbus_connection_call_with_unix_fd_list_sync() failed: {}",
error->message);
g_error_free(error);
handle = -1;
} else {
gint index;
g_variant_get(reply, "(h)", &index);
g_variant_unref(reply);
handle = g_unix_fd_list_get(fd_list, index, nullptr);
g_object_unref(fd_list);
}
return handle;
}
auto checkInhibitor(const std::string& inhibitor) -> const std::string& {
static const auto inhibitors = std::array{
"idle",
"shutdown",
"sleep",
"handle-power-key",
"handle-suspend-key",
"handle-hibernate-key",
"handle-lid-switch"
};
if (std::find(inhibitors.begin(), inhibitors.end(), inhibitor)
== inhibitors.end()) {
throw std::runtime_error("invalid logind inhibitor " + inhibitor);
}
return inhibitor;
}
auto getInhibitors(const Json::Value& config) -> std::string {
std::string inhibitors = "idle";
if (config["what"].empty()) {
return inhibitors;
}
if (config["what"].isString()) {
return checkInhibitor(config["what"].asString());
}
if (config["what"].isArray()) {
inhibitors = checkInhibitor(config["what"][0].asString());
for (decltype(config["what"].size()) i = 1; i < config["what"].size(); ++i) {
inhibitors += ":" + checkInhibitor(config["what"][i].asString());
}
return inhibitors;
}
return inhibitors;
}
}
namespace waybar::modules {
Inhibitor::Inhibitor(const std::string& id, const Bar& bar,
const Json::Value& config)
: ALabel(config, "inhibitor", id, "{status}", true),
dbus_(::dbus()),
inhibitors_(::getInhibitors(config)) {
event_box_.add_events(Gdk::BUTTON_PRESS_MASK);
event_box_.signal_button_press_event().connect(
sigc::mem_fun(*this, &Inhibitor::handleToggle));
dp.emit();
}
Inhibitor::~Inhibitor() {
if (handle_ != -1) {
::close(handle_);
}
}
auto Inhibitor::activated() -> bool {
return handle_ != -1;
}
auto Inhibitor::update() -> void {
std::string status_text = activated() ? "activated" : "deactivated";
label_.get_style_context()->remove_class(
activated() ? "deactivated" : "activated");
label_.set_markup(
fmt::format(format_, fmt::arg("status", status_text),
fmt::arg("icon", getIcon(0, status_text))));
label_.get_style_context()->add_class(status_text);
if (tooltipEnabled()) {
label_.set_tooltip_text(status_text);
}
return ALabel::update();
}
auto Inhibitor::handleToggle(GdkEventButton* const& e) -> bool {
if (e->button == 1) {
if (activated()) {
::close(handle_);
handle_ = -1;
} else {
handle_ = ::getLocks(dbus_, inhibitors_);
if (handle_ == -1) {
spdlog::error("cannot get inhibitor locks");
}
}
}
return ALabel::handleToggle(e);
}
} // waybar::modules

View File

@ -131,6 +131,9 @@ void waybar::modules::MPD::setLabel() {
date = getTag(MPD_TAG_DATE); date = getTag(MPD_TAG_DATE);
song_pos = mpd_status_get_song_pos(status_.get()); song_pos = mpd_status_get_song_pos(status_.get());
volume = mpd_status_get_volume(status_.get()); volume = mpd_status_get_volume(status_.get());
if (volume < 0) {
volume = 0;
}
queue_length = mpd_status_get_queue_length(status_.get()); queue_length = mpd_status_get_queue_length(status_.get());
elapsedTime = std::chrono::seconds(mpd_status_get_elapsed_time(status_.get())); elapsedTime = std::chrono::seconds(mpd_status_get_elapsed_time(status_.get()));
totalTime = std::chrono::seconds(mpd_status_get_total_time(status_.get())); totalTime = std::chrono::seconds(mpd_status_get_total_time(status_.get()));

View File

@ -1,87 +1,77 @@
#include "modules/network.hpp" #include <linux/if.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <sys/eventfd.h> #include <sys/eventfd.h>
#include <linux/if.h>
#include <fstream>
#include <cassert> #include <cassert>
#include <fstream>
#include <sstream>
#include <optional> #include <optional>
#include "modules/network.hpp"
#include "util/format.hpp" #include "util/format.hpp"
#ifdef WANT_RFKILL #ifdef WANT_RFKILL
#include "util/rfkill.hpp" #include "util/rfkill.hpp"
#endif #endif
namespace { namespace {
using namespace waybar::util; using namespace waybar::util;
constexpr const char *NETSTAT_FILE =
"/proc/net/netstat"; // std::ifstream does not take std::string_view as param
constexpr std::string_view BANDWIDTH_CATEGORY = "IpExt";
constexpr std::string_view BANDWIDTH_DOWN_TOTAL_KEY = "InOctets";
constexpr std::string_view BANDWIDTH_UP_TOTAL_KEY = "OutOctets";
constexpr const char *DEFAULT_FORMAT = "{ifname}"; constexpr const char *DEFAULT_FORMAT = "{ifname}";
std::ifstream netstat(NETSTAT_FILE);
std::optional<unsigned long long> read_netstat(std::string_view category, std::string_view key) {
if (!netstat) {
spdlog::warn("Failed to open netstat file {}", NETSTAT_FILE);
return {};
}
netstat.seekg(std::ios_base::beg);
// finding corresponding line (category)
// looks into the file for the first line starting by the 'category' string
auto starts_with = [](const std::string &str, std::string_view start) {
return start == std::string_view{str.data(), std::min(str.size(), start.size())};
};
std::string read;
while (std::getline(netstat, read) && !starts_with(read, category))
;
if (!starts_with(read, category)) {
spdlog::warn("Category '{}' not found in netstat file {}", category, NETSTAT_FILE);
return {};
}
// finding corresponding column (key)
// looks into the fetched line for the first word (space separated) equal to 'key'
int index = 0;
auto r_it = read.begin();
auto k_it = key.begin();
while (k_it != key.end() && r_it != read.end()) {
if (*r_it != *k_it) {
r_it = std::find(r_it, read.end(), ' ');
if (r_it != read.end()) {
++r_it;
}
k_it = key.begin();
++index;
} else {
++r_it;
++k_it;
}
}
if (r_it == read.end() && k_it != key.end()) {
spdlog::warn(
"Key '{}' not found in category '{}' of netstat file {}", key, category, NETSTAT_FILE);
return {};
}
// finally accessing value
// accesses the line right under the fetched one
std::getline(netstat, read);
assert(starts_with(read, category));
std::istringstream iss(read);
while (index--) {
std::getline(iss, read, ' ');
}
unsigned long long value;
iss >> value;
return value;
}
} // namespace } // namespace
constexpr const char *NETDEV_FILE =
"/proc/net/dev"; // std::ifstream does not take std::string_view as param
std::optional<std::pair<unsigned long long, unsigned long long>>
waybar::modules::Network::readBandwidthUsage() {
std::ifstream netdev(NETDEV_FILE);
if (!netdev) {
spdlog::warn("Failed to open netdev file {}", NETDEV_FILE);
return {};
}
std::string line;
// skip the headers (first two lines)
std::getline(netdev, line);
std::getline(netdev, line);
unsigned long long receivedBytes = 0ull;
unsigned long long transmittedBytes = 0ull;
while (std::getline(netdev, line)) {
std::istringstream iss(line);
std::string ifacename;
iss >> ifacename; // ifacename contains "eth0:"
ifacename.pop_back(); // remove trailing ':'
if (!checkInterface(ifacename)) {
continue;
}
// The rest of the line consists of whitespace separated counts divided
// into two groups (receive and transmit). Each group has the following
// columns: bytes, packets, errs, drop, fifo, frame, compressed, multicast
//
// We only care about the bytes count, so we'll just ignore the 7 other
// columns.
unsigned long long r = 0ull;
unsigned long long t = 0ull;
// Read received bytes
iss >> r;
// Skip all the other columns in the received group
for (int colsToSkip = 7; colsToSkip > 0; colsToSkip--) {
// skip whitespace between columns
while (iss.peek() == ' ') { iss.ignore(); }
// skip the irrelevant column
while (iss.peek() != ' ') { iss.ignore(); }
}
// Read transmit bytes
iss >> t;
receivedBytes += r;
transmittedBytes += t;
}
return {{receivedBytes, transmittedBytes}};
}
waybar::modules::Network::Network(const std::string &id, const Json::Value &config) waybar::modules::Network::Network(const std::string &id, const Json::Value &config)
: ALabel(config, "network", id, DEFAULT_FORMAT, 60), : ALabel(config, "network", id, DEFAULT_FORMAT, 60),
ifid_(-1), ifid_(-1),
@ -98,7 +88,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
#ifdef WANT_RFKILL #ifdef WANT_RFKILL
rfkill_{RFKILL_TYPE_WLAN}, rfkill_{RFKILL_TYPE_WLAN},
#endif #endif
frequency_(0) { frequency_(0.0) {
// Start with some "text" in the module's label_, update() will then // Start with some "text" in the module's label_, update() will then
// update it. Since the text should be different, update() will be able // update it. Since the text should be different, update() will be able
@ -106,17 +96,12 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf
// the module start with no text, but the the event_box_ is shown. // the module start with no text, but the the event_box_ is shown.
label_.set_markup("<s></s>"); label_.set_markup("<s></s>");
auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY); auto bandwidth = readBandwidthUsage();
auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY); if (bandwidth.has_value()) {
if (down_octets) { bandwidth_down_total_ = (*bandwidth).first;
bandwidth_down_total_ = *down_octets; bandwidth_up_total_ = (*bandwidth).second;
} else { } else {
bandwidth_down_total_ = 0; bandwidth_down_total_ = 0;
}
if (up_octets) {
bandwidth_up_total_ = *up_octets;
} else {
bandwidth_up_total_ = 0; bandwidth_up_total_ = 0;
} }
@ -303,20 +288,21 @@ const std::string waybar::modules::Network::getNetworkState() const {
auto waybar::modules::Network::update() -> void { auto waybar::modules::Network::update() -> void {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
std::string tooltip_format; std::string tooltip_format;
auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY);
auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY);
unsigned long long bandwidth_down = 0; auto bandwidth = readBandwidthUsage();
if (down_octets) { auto bandwidth_down = 0ull;
bandwidth_down = *down_octets - bandwidth_down_total_; auto bandwidth_up = 0ull;
bandwidth_down_total_ = *down_octets; if (bandwidth.has_value()) {
auto down_octets = (*bandwidth).first;
auto up_octets = (*bandwidth).second;
bandwidth_down = down_octets - bandwidth_down_total_;
bandwidth_down_total_ = down_octets;
bandwidth_up = up_octets - bandwidth_up_total_;
bandwidth_up_total_ = up_octets;
} }
unsigned long long bandwidth_up = 0;
if (up_octets) {
bandwidth_up = *up_octets - bandwidth_up_total_;
bandwidth_up_total_ = *up_octets;
}
if (!alt_) { if (!alt_) {
auto state = getNetworkState(); auto state = getNetworkState();
if (!state_.empty() && label_.get_style_context()->has_class(state_)) { if (!state_.empty() && label_.get_style_context()->has_class(state_)) {
@ -345,12 +331,13 @@ auto waybar::modules::Network::update() -> void {
fmt::arg("essid", essid_), fmt::arg("essid", essid_),
fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signaldBm", signal_strength_dbm_),
fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrength", signal_strength_),
fmt::arg("signalStrengthApp", signal_strength_app_),
fmt::arg("ifname", ifname_), fmt::arg("ifname", ifname_),
fmt::arg("netmask", netmask_), fmt::arg("netmask", netmask_),
fmt::arg("ipaddr", ipaddr_), fmt::arg("ipaddr", ipaddr_),
fmt::arg("gwaddr", gwaddr_), fmt::arg("gwaddr", gwaddr_),
fmt::arg("cidr", cidr_), fmt::arg("cidr", cidr_),
fmt::arg("frequency", frequency_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("icon", getIcon(signal_strength_, state_)),
fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")),
fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")),
@ -374,12 +361,13 @@ auto waybar::modules::Network::update() -> void {
fmt::arg("essid", essid_), fmt::arg("essid", essid_),
fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signaldBm", signal_strength_dbm_),
fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrength", signal_strength_),
fmt::arg("signalStrengthApp", signal_strength_app_),
fmt::arg("ifname", ifname_), fmt::arg("ifname", ifname_),
fmt::arg("netmask", netmask_), fmt::arg("netmask", netmask_),
fmt::arg("ipaddr", ipaddr_), fmt::arg("ipaddr", ipaddr_),
fmt::arg("gwaddr", gwaddr_), fmt::arg("gwaddr", gwaddr_),
fmt::arg("cidr", cidr_), fmt::arg("cidr", cidr_),
fmt::arg("frequency", frequency_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)),
fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("icon", getIcon(signal_strength_, state_)),
fmt::arg("bandwidthDownBits", fmt::arg("bandwidthDownBits",
pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")),
@ -417,7 +405,8 @@ void waybar::modules::Network::clearIface() {
cidr_ = 0; cidr_ = 0;
signal_strength_dbm_ = 0; signal_strength_dbm_ = 0;
signal_strength_ = 0; signal_strength_ = 0;
frequency_ = 0; signal_strength_app_.clear();
frequency_ = 0.0;
} }
int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
@ -484,7 +473,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) {
net->essid_.clear(); net->essid_.clear();
net->signal_strength_dbm_ = 0; net->signal_strength_dbm_ = 0;
net->signal_strength_ = 0; net->signal_strength_ = 0;
net->frequency_ = 0; net->signal_strength_app_.clear();
net->frequency_ = 0.0;
} }
} }
net->carrier_ = carrier.value(); net->carrier_ = carrier.value();
@ -802,13 +792,30 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) {
if (bss[NL80211_BSS_SIGNAL_MBM] != nullptr) { if (bss[NL80211_BSS_SIGNAL_MBM] != nullptr) {
// signalstrength in dBm from mBm // signalstrength in dBm from mBm
signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100; signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100;
// WiFi-hardware usually operates in the range -90 to -30dBm.
// WiFi-hardware usually operates in the range -90 to -20dBm. // If a signal is too strong, it can overwhelm receiving circuity that is designed
const int hardwareMax = -20; // to pick up and process a certain signal level. The following percentage is scaled to
// punish signals that are too strong (>= -45dBm) or too weak (<= -45 dBm).
const int hardwareOptimum = -45;
const int hardwareMin = -90; const int hardwareMin = -90;
const int strength = const int strength =
((signal_strength_dbm_ - hardwareMin) / double{hardwareMax - hardwareMin}) * 100; 100 - ((abs(signal_strength_dbm_ - hardwareOptimum) / double{hardwareOptimum - hardwareMin}) * 100);
signal_strength_ = std::clamp(strength, 0, 100); signal_strength_ = std::clamp(strength, 0, 100);
if (signal_strength_dbm_ >= -50) {
signal_strength_app_ = "Great Connectivity";
} else if (signal_strength_dbm_ >= -60) {
signal_strength_app_ = "Good Connectivity";
} else if (signal_strength_dbm_ >= -67) {
signal_strength_app_ = "Streaming";
} else if (signal_strength_dbm_ >= -70) {
signal_strength_app_ = "Web Surfing";
} else if (signal_strength_dbm_ >= -80) {
signal_strength_app_ = "Basic Connectivity";
} else {
signal_strength_app_ = "Poor Connectivity";
}
} }
if (bss[NL80211_BSS_SIGNAL_UNSPEC] != nullptr) { if (bss[NL80211_BSS_SIGNAL_UNSPEC] != nullptr) {
signal_strength_ = nla_get_u8(bss[NL80211_BSS_SIGNAL_UNSPEC]); signal_strength_ = nla_get_u8(bss[NL80211_BSS_SIGNAL_UNSPEC]);
@ -817,8 +824,8 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) {
void waybar::modules::Network::parseFreq(struct nlattr **bss) { void waybar::modules::Network::parseFreq(struct nlattr **bss) {
if (bss[NL80211_BSS_FREQUENCY] != nullptr) { if (bss[NL80211_BSS_FREQUENCY] != nullptr) {
// in MHz // in GHz
frequency_ = nla_get_u32(bss[NL80211_BSS_FREQUENCY]); frequency_ = (double) nla_get_u32(bss[NL80211_BSS_FREQUENCY]) / 1000;
} }
} }

View File

@ -79,6 +79,13 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
if (dir == SCROLL_DIR::NONE) { if (dir == SCROLL_DIR::NONE) {
return true; return true;
} }
if (config_["reverse-scrolling"].asInt() == 1){
if (dir == SCROLL_DIR::UP) {
dir = SCROLL_DIR::DOWN;
} else if (dir == SCROLL_DIR::DOWN) {
dir = SCROLL_DIR::UP;
}
}
double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100; double volume_tick = static_cast<double>(PA_VOLUME_NORM) / 100;
pa_volume_t change = volume_tick; pa_volume_t change = volume_tick;
pa_cvolume pa_volume = pa_volume_; pa_cvolume pa_volume = pa_volume_;
@ -211,7 +218,7 @@ static const std::array<std::string, 9> ports = {
}; };
const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const { const std::vector<std::string> waybar::modules::Pulseaudio::getPulseIcon() const {
std::vector<std::string> res = {default_source_name_}; std::vector<std::string> res = {current_sink_name_, default_source_name_};
std::string nameLC = port_name_ + form_factor_; std::string nameLC = port_name_ + form_factor_;
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
for (auto const &port : ports) { for (auto const &port : ports) {

View File

@ -7,7 +7,6 @@
#include "client.hpp" #include "client.hpp"
#include "modules/river/tags.hpp" #include "modules/river/tags.hpp"
#include "river-status-unstable-v1-client-protocol.h"
#include "xdg-output-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h"
namespace waybar::modules::river { namespace waybar::modules::river {
@ -33,6 +32,23 @@ static const zriver_output_status_v1_listener output_status_listener_impl{
.urgent_tags = listen_urgent_tags, .urgent_tags = listen_urgent_tags,
}; };
static void listen_command_success(void *data,
struct zriver_command_callback_v1 *zriver_command_callback_v1,
const char *output) {
// Do nothing but keep listener to avoid crashing when command was successful
}
static void listen_command_failure(void *data,
struct zriver_command_callback_v1 *zriver_command_callback_v1,
const char *output) {
spdlog::error("failure when selecting/toggling tags {}", output);
}
static const zriver_command_callback_v1_listener command_callback_listener_impl {
.success = listen_command_success,
.failure = listen_command_failure,
};
static void handle_global(void *data, struct wl_registry *registry, uint32_t name, static void handle_global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) { const char *interface, uint32_t version) {
if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) {
@ -43,18 +59,33 @@ static void handle_global(void *data, struct wl_registry *registry, uint32_t nam
static_cast<Tags *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>( static_cast<Tags *>(data)->status_manager_ = static_cast<struct zriver_status_manager_v1 *>(
wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version)); wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version));
} }
if (std::strcmp(interface, zriver_control_v1_interface.name) == 0) {
version = std::min<uint32_t>(version, 1);
static_cast<Tags *>(data)->control_ = static_cast<struct zriver_control_v1 *>(
wl_registry_bind(registry, name, &zriver_control_v1_interface, version));
}
if (std::strcmp(interface, wl_seat_interface.name) == 0) {
version = std::min<uint32_t>(version, 1);
static_cast<Tags *>(data)->seat_ = static_cast<struct wl_seat *>(
wl_registry_bind(registry, name, &wl_seat_interface, version));
}
} }
static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
/* Ignore event */ /* Ignore event */
} }
static const wl_registry_listener registry_listener_impl = {.global = handle_global, static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove}; .global_remove = handle_global_remove};
Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config) Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config)
: waybar::AModule(config, "tags", id, false, false), : waybar::AModule(config, "tags", id, false, false),
status_manager_{nullptr}, status_manager_{nullptr},
control_{nullptr},
seat_{nullptr},
bar_(bar), bar_(bar),
box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0},
output_status_{nullptr} { output_status_{nullptr} {
@ -68,6 +99,14 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
return; return;
} }
if (!control_) {
spdlog::error("river_control_v1 not advertised");
}
if (!seat_) {
spdlog::error("wl_seat not advertised");
}
box_.set_name("tags"); box_.set_name("tags");
if (!id.empty()) { if (!id.empty()) {
box_.get_style_context()->add_class(id); box_.get_style_context()->add_class(id);
@ -89,11 +128,17 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con
} }
} }
uint32_t i = 1;
for (const auto &tag_label : tag_labels) { for (const auto &tag_label : tag_labels) {
Gtk::Button &button = buttons_.emplace_back(tag_label); Gtk::Button &button = buttons_.emplace_back(tag_label);
button.set_relief(Gtk::RELIEF_NONE); button.set_relief(Gtk::RELIEF_NONE);
box_.pack_start(button, false, false, 0); box_.pack_start(button, false, false, 0);
if (!config_["disable-click"].asBool()) {
button.signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i));
button.signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i));
}
button.show(); button.show();
i <<= 1;
} }
struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
@ -107,6 +152,31 @@ Tags::~Tags() {
if (output_status_) { if (output_status_) {
zriver_output_status_v1_destroy(output_status_); zriver_output_status_v1_destroy(output_status_);
} }
if (control_) {
zriver_control_v1_destroy(control_);
}
}
void Tags::handle_primary_clicked(uint32_t tag) {
// Send river command to select tag on left mouse click
zriver_command_callback_v1 *callback;
zriver_control_v1_add_argument(control_, "set-focused-tags");
zriver_control_v1_add_argument(control_, std::to_string(tag).c_str());
callback = zriver_control_v1_run_command(control_, seat_);
zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr);
}
bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) {
if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) {
// Send river command to toggle tag on right mouse click
zriver_command_callback_v1 *callback;
zriver_control_v1_add_argument(control_, "toggle-focused-tags");
zriver_control_v1_add_argument(control_, std::to_string(tag).c_str());
callback = zriver_control_v1_run_command(control_, seat_);
zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr);
}
return true;
} }
void Tags::handle_focused_tags(uint32_t tags) { void Tags::handle_focused_tags(uint32_t tags) {

107
src/modules/sway/bar.cpp Normal file
View File

@ -0,0 +1,107 @@
#include "modules/sway/bar.hpp"
#include <fmt/ostream.h>
#include <spdlog/spdlog.h>
#include <stdexcept>
#include "bar.hpp"
#include "modules/sway/ipc/ipc.hpp"
namespace waybar::modules::sway {
BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} {
{
sigc::connection handle =
ipc_.signal_cmd.connect(sigc::mem_fun(*this, &BarIpcClient::onInitialConfig));
ipc_.sendCmd(IPC_GET_BAR_CONFIG, bar_.bar_id);
handle.disconnect();
}
signal_config_.connect(sigc::mem_fun(*this, &BarIpcClient::onConfigUpdate));
signal_visible_.connect(sigc::mem_fun(*this, &BarIpcClient::onVisibilityUpdate));
ipc_.subscribe(R"(["bar_state_update", "barconfig_update"])");
ipc_.signal_event.connect(sigc::mem_fun(*this, &BarIpcClient::onIpcEvent));
// Launch worker
ipc_.setWorker([this] {
try {
ipc_.handleEvent();
} catch (const std::exception& e) {
spdlog::error("BarIpcClient::handleEvent {}", e.what());
}
});
}
struct swaybar_config parseConfig(const Json::Value& payload) {
swaybar_config conf;
if (auto id = payload["id"]; id.isString()) {
conf.id = id.asString();
}
if (auto mode = payload["mode"]; mode.isString()) {
conf.mode = mode.asString();
}
if (auto hs = payload["hidden_state"]; hs.isString()) {
conf.hidden_state = hs.asString();
}
return conf;
}
void BarIpcClient::onInitialConfig(const struct Ipc::ipc_response& res) {
auto payload = parser_.parse(res.payload);
if (auto success = payload.get("success", true); !success.asBool()) {
auto err = payload.get("error", "Unknown error");
throw std::runtime_error(err.asString());
}
auto config = parseConfig(payload);
onConfigUpdate(config);
}
void BarIpcClient::onIpcEvent(const struct Ipc::ipc_response& res) {
try {
auto payload = parser_.parse(res.payload);
if (auto id = payload["id"]; id.isString() && id.asString() != bar_.bar_id) {
spdlog::trace("swaybar ipc: ignore event for {}", id.asString());
return;
}
if (payload.isMember("visible_by_modifier")) {
// visibility change for hidden bar
signal_visible_(payload["visible_by_modifier"].asBool());
} else {
// configuration update
auto config = parseConfig(payload);
signal_config_(std::move(config));
}
} catch (const std::exception& e) {
spdlog::error("BarIpcClient::onEvent {}", e.what());
}
}
void BarIpcClient::onConfigUpdate(const swaybar_config& config) {
spdlog::info("config update for {}: id {}, mode {}, hidden_state {}",
bar_.bar_id,
config.id,
config.mode,
config.hidden_state);
bar_config_ = config;
update();
}
void BarIpcClient::onVisibilityUpdate(bool visible_by_modifier) {
spdlog::debug("visiblity update for {}: {}", bar_.bar_id, visible_by_modifier);
visible_by_modifier_ = visible_by_modifier;
update();
}
void BarIpcClient::update() {
bool visible = visible_by_modifier_;
if (bar_config_.mode == "invisible") {
visible = false;
} else if (bar_config_.mode != "hide" || bar_config_.hidden_state != "hide") {
visible = true;
}
bar_.setMode(visible ? bar_config_.mode : Bar::MODE_INVISIBLE);
}
} // namespace waybar::modules::sway

View File

@ -99,7 +99,8 @@ auto Language::update() -> void {
fmt::arg("short", layout_.short_name), fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description), fmt::arg("shortDescription", layout_.short_description),
fmt::arg("long", layout_.full_name), fmt::arg("long", layout_.full_name),
fmt::arg("variant", layout_.variant))); fmt::arg("variant", layout_.variant),
fmt::arg("flag", layout_.country_flag())));
label_.set_markup(display_layout); label_.set_markup(display_layout);
if (tooltipEnabled()) { if (tooltipEnabled()) {
if (tooltip_format_ != "") { if (tooltip_format_ != "") {
@ -107,7 +108,8 @@ auto Language::update() -> void {
fmt::arg("short", layout_.short_name), fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description), fmt::arg("shortDescription", layout_.short_description),
fmt::arg("long", layout_.full_name), fmt::arg("long", layout_.full_name),
fmt::arg("variant", layout_.variant))); fmt::arg("variant", layout_.variant),
fmt::arg("flag", layout_.country_flag())));
label_.set_tooltip_markup(tooltip_display_layout); label_.set_tooltip_markup(tooltip_display_layout);
} else { } else {
label_.set_tooltip_markup(display_layout); label_.set_tooltip_markup(display_layout);
@ -212,4 +214,15 @@ Language::XKBContext::~XKBContext() {
rxkb_context_unref(context_); rxkb_context_unref(context_);
delete layout_; delete layout_;
} }
std::string Language::Layout::country_flag() const {
if (short_name.size() != 2) return "";
unsigned char result[] = "\xf0\x9f\x87\x00\xf0\x9f\x87\x00";
result[3] = short_name[0] + 0x45;
result[7] = short_name[1] + 0x45;
// Check if both emojis are in A-Z symbol bounds
if (result[3] < 0xa6 || result[3] > 0xbf) return "";
if (result[7] < 0xa6 || result[7] > 0xbf) return "";
return std::string{reinterpret_cast<char*>(result)};
}
} // namespace waybar::modules::sway } // namespace waybar::modules::sway

View File

@ -2,6 +2,7 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm>
#include <cctype> #include <cctype>
#include <string> #include <string>
@ -98,6 +99,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
Json::Value v; Json::Value v;
v["name"] = p_w_name; v["name"] = p_w_name;
v["target_output"] = bar_.output->name; v["target_output"] = bar_.output->name;
v["num"] = convertWorkspaceNameToNum(p_w_name);
workspaces_.emplace_back(std::move(v)); workspaces_.emplace_back(std::move(v));
break; break;
} }
@ -107,57 +109,59 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) {
Json::Value v; Json::Value v;
v["name"] = p_w_name; v["name"] = p_w_name;
v["target_output"] = ""; v["target_output"] = "";
v["num"] = convertWorkspaceNameToNum(p_w_name);
workspaces_.emplace_back(std::move(v)); workspaces_.emplace_back(std::move(v));
} }
} }
} }
// config option to sort numeric workspace names before others // sway has a defined ordering of workspaces that should be preserved in
bool config_numeric_first = config_["numeric-first"].asBool(); // the representation displayed by waybar to ensure that commands such
// as "workspace prev" or "workspace next" make sense when looking at
// the workspace representation in the bar.
// Due to waybar's own feature of persistent workspaces unknown to sway,
// custom sorting logic is necessary to make these workspaces appear
// naturally in the list of workspaces without messing up sway's
// sorting. For this purpose, a custom numbering property is created
// that preserves the order provided by sway while inserting numbered
// persistent workspaces at their natural positions.
//
// All of this code assumes that sway provides numbered workspaces first
// and other workspaces are sorted by their creation time.
//
// In a first pass, the maximum "num" value is computed to enqueue
// unnumbered workspaces behind numbered ones when computing the sort
// attribute.
int max_num = -1;
for (auto & workspace : workspaces_) {
max_num = std::max(workspace["num"].asInt(), max_num);
}
for (auto & workspace : workspaces_) {
auto workspace_num = workspace["num"].asInt();
if (workspace_num > -1) {
workspace["sort"] = workspace_num;
} else {
workspace["sort"] = ++max_num;
}
}
std::sort(workspaces_.begin(), std::sort(workspaces_.begin(),
workspaces_.end(), workspaces_.end(),
[config_numeric_first](const Json::Value &lhs, const Json::Value &rhs) { [](const Json::Value &lhs, const Json::Value &rhs) {
// the "num" property (integer type):
// The workspace number or -1 for workspaces that do
// not start with a number.
// We could rely on sway providing this property:
//
// auto l = lhs["num"].asInt();
// auto r = rhs["num"].asInt();
//
// We cannot rely on the "num" property as provided by sway
// via IPC, because persistent workspace might not exist in
// sway's view. However, we need this property also for
// not-yet created persistent workspace. As such, we simply
// duplicate sway's logic of assigning the "num" property
// into waybar (see convertWorkspaceNameToNum). This way the
// sorting should work out even when we include workspaces
// that do not currently exist.
auto lname = lhs["name"].asString(); auto lname = lhs["name"].asString();
auto rname = rhs["name"].asString(); auto rname = rhs["name"].asString();
int l = convertWorkspaceNameToNum(lname); int l = lhs["sort"].asInt();
int r = convertWorkspaceNameToNum(rname); int r = rhs["sort"].asInt();
if (l == r) { if (l == r) {
// in case both integers are the same, lexicographical // In case both integers are the same, lexicographical
// sort. This also covers the case when both don't have a // sort. The code above already ensure that this will only
// number (i.e., l == r == -1). // happend in case of explicitly numbered workspaces.
return lname < rname; return lname < rname;
} }
// one of the workspaces doesn't begin with a number, so
// num is -1.
if (l < 0 || r < 0) {
if (config_numeric_first) {
return r < 0;
}
return l < 0;
}
// both workspaces have a "num" so let's just compare those
return l < r; return l < r;
}); });
} }
dp.emit(); dp.emit();
} catch (const std::exception &e) { } catch (const std::exception &e) {

View File

@ -11,7 +11,9 @@
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <utility>
#include <fmt/core.h>
#include <gdkmm/monitor.h> #include <gdkmm/monitor.h>
#include <gtkmm/icontheme.h> #include <gtkmm/icontheme.h>
@ -86,8 +88,7 @@ static Glib::RefPtr<Gdk::Pixbuf> load_icon_from_file(std::string icon_path, int
} }
} }
/* Method 1 - get the correct icon name from the desktop file */ static Glib::RefPtr<Gio::DesktopAppInfo> get_app_info_by_name(const std::string& app_id)
static std::string get_from_desktop_app_info(const std::string &app_id)
{ {
static std::vector<std::string> prefixes = search_prefix(); static std::vector<std::string> prefixes = search_prefix();
@ -103,33 +104,29 @@ static std::string get_from_desktop_app_info(const std::string &app_id)
".desktop" ".desktop"
}; };
Glib::RefPtr<Gio::DesktopAppInfo> app_info; for (auto& prefix : prefixes) {
for (auto& folder : app_folders) {
for (auto& suffix : suffixes) {
auto app_info_ = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix);
if (!app_info_) {
continue;
}
for (auto& prefix : prefixes) return app_info_;
for (auto& folder : app_folders) }
for (auto& suffix : suffixes) }
if (!app_info) }
app_info = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix);
if (app_info && app_info->get_icon()) return {};
return app_info->get_icon()->to_string();
return "";
} }
/* Method 2 - use the app_id and check whether there is an icon with this name in the icon theme */ Glib::RefPtr<Gio::DesktopAppInfo> get_desktop_app_info(const std::string &app_id)
static std::string get_from_icon_theme(const Glib::RefPtr<Gtk::IconTheme>& icon_theme,
const std::string &app_id)
{ {
if (icon_theme->lookup_icon(app_id, 24)) auto app_info = get_app_info_by_name(app_id);
return app_id; if (app_info) {
return app_info;
}
return "";
}
/* Method 3 - as last resort perform a search for most appropriate desktop info file */
static std::string get_from_desktop_app_info_search(const std::string &app_id)
{
std::string desktop_file = ""; std::string desktop_file = "";
gchar*** desktop_list = g_desktop_app_info_search(app_id.c_str()); gchar*** desktop_list = g_desktop_app_info_search(app_id.c_str());
@ -151,65 +148,84 @@ static std::string get_from_desktop_app_info_search(const std::string &app_id)
} }
g_free(desktop_list); g_free(desktop_list);
return get_from_desktop_app_info(desktop_file); return get_app_info_by_name(desktop_file);
} }
static bool image_load_icon(Gtk::Image& image, const Glib::RefPtr<Gtk::IconTheme>& icon_theme, void Task::set_app_info_from_app_id_list(const std::string& app_id_list) {
const std::string &app_id_list, int size)
{
std::string app_id; std::string app_id;
std::istringstream stream(app_id_list); std::istringstream stream(app_id_list);
bool found = false;
/* Wayfire sends a list of app-id's in space separated format, other compositors /* Wayfire sends a list of app-id's in space separated format, other compositors
* send a single app-id, but in any case this works fine */ * send a single app-id, but in any case this works fine */
while (stream >> app_id) while (stream >> app_id)
{ {
app_info_ = get_desktop_app_info(app_id);
if (app_info_) {
return;
}
auto lower_app_id = app_id;
std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(),
[](char c){ return std::tolower(c); });
app_info_ = get_desktop_app_info(lower_app_id);
if (app_info_) {
return;
}
size_t start = 0, end = app_id.size(); size_t start = 0, end = app_id.size();
start = app_id.rfind(".", end); start = app_id.rfind(".", end);
std::string app_name = app_id.substr(start+1, app_id.size()); std::string app_name = app_id.substr(start+1, app_id.size());
app_info_ = get_desktop_app_info(app_name);
if (app_info_) {
return;
}
auto lower_app_id = app_id; start = app_id.find("-");
std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), app_name = app_id.substr(0, start);
[](char c){ return std::tolower(c); }); app_info_ = get_desktop_app_info(app_name);
}
}
std::string icon_name = get_from_icon_theme(icon_theme, app_id); static std::string get_icon_name_from_icon_theme(const Glib::RefPtr<Gtk::IconTheme>& icon_theme,
const std::string &app_id)
{
if (icon_theme->lookup_icon(app_id, 24))
return app_id;
if (icon_name.empty()) return "";
icon_name = get_from_icon_theme(icon_theme, lower_app_id); }
if (icon_name.empty())
icon_name = get_from_icon_theme(icon_theme, app_name);
if (icon_name.empty())
icon_name = get_from_desktop_app_info(app_id);
if (icon_name.empty())
icon_name = get_from_desktop_app_info(lower_app_id);
if (icon_name.empty())
icon_name = get_from_desktop_app_info(app_name);
if (icon_name.empty())
icon_name = get_from_desktop_app_info_search(app_id);
if (icon_name.empty()) bool Task::image_load_icon(Gtk::Image& image, const Glib::RefPtr<Gtk::IconTheme>& icon_theme, Glib::RefPtr<Gio::DesktopAppInfo> app_info, int size)
icon_name = "unknown"; {
std::string ret_icon_name = "unknown";
if (app_info) {
std::string icon_name = get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class());
if (!icon_name.empty()) {
ret_icon_name = icon_name;
} else {
if (app_info->get_icon()) {
ret_icon_name = app_info->get_icon()->to_string();
}
}
}
Glib::RefPtr<Gdk::Pixbuf> pixbuf; Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try { try {
pixbuf = icon_theme->load_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); pixbuf = icon_theme->load_icon(ret_icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE);
} catch(...) { } catch(...) {
if (Glib::file_test(icon_name, Glib::FILE_TEST_EXISTS)) if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS))
pixbuf = load_icon_from_file(icon_name, size); pixbuf = load_icon_from_file(ret_icon_name, size);
else else
pixbuf = {}; pixbuf = {};
} }
if (pixbuf) { if (pixbuf) {
image.set(pixbuf); image.set(pixbuf);
found = true; return true;
break; }
}
}
return found; return false;
} }
/* Task class implementation */ /* Task class implementation */
@ -276,8 +292,7 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar,
struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) : struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) :
bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat}, bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat},
id_{global_id++}, id_{global_id++},
content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}
button_visible_{false}, ignored_{false}
{ {
zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this);
@ -290,13 +305,15 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar,
content_.show(); content_.show();
button_.add(content_); button_.add(content_);
with_icon_ = false;
format_before_.clear(); format_before_.clear();
format_after_.clear(); format_after_.clear();
if (config_["format"].isString()) { if (config_["format"].isString()) {
/* The user defined a format string, use it */ /* The user defined a format string, use it */
auto format = config_["format"].asString(); auto format = config_["format"].asString();
if (format.find("{name}") != std::string::npos) {
with_name_ = true;
}
auto icon_pos = format.find("{icon}"); auto icon_pos = format.find("{icon}");
if (icon_pos == 0) { if (icon_pos == 0) {
@ -377,13 +394,12 @@ std::string Task::state_string(bool shortened) const
void Task::handle_title(const char *title) void Task::handle_title(const char *title)
{ {
title_ = title; title_ = title;
hide_if_ignored();
} }
void Task::handle_app_id(const char *app_id) void Task::hide_if_ignored()
{ {
app_id_ = app_id; if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) {
if (tbar_->ignore_list().count(app_id)) {
ignored_ = true; ignored_ = true;
if (button_visible_) { if (button_visible_) {
auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj());
@ -397,14 +413,35 @@ void Task::handle_app_id(const char *app_id)
handle_output_enter(output); handle_output_enter(output);
} }
} }
}
if (!with_icon_) void Task::handle_app_id(const char *app_id)
{
app_id_ = app_id;
hide_if_ignored();
auto ids_replace_map = tbar_->app_ids_replace_map();
if (ids_replace_map.count(app_id_)) {
auto replaced_id = ids_replace_map[app_id_];
spdlog::debug(fmt::format("Task ({}) [{}] app_id was replaced with {}", id_, app_id_, replaced_id));
app_id_ = replaced_id;
}
if (!with_icon_ && !with_name_) {
return; return;
}
set_app_info_from_app_id_list(app_id_);
name_ = app_info_ ? app_info_->get_display_name() : app_id;
if (!with_icon_) {
return;
}
int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16;
bool found = false; bool found = false;
for (auto& icon_theme : tbar_->icon_themes()) { for (auto& icon_theme : tbar_->icon_themes()) {
if (image_load_icon(icon_, icon_theme, app_id_, icon_size)) { if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) {
found = true; found = true;
break; break;
} }
@ -418,13 +455,13 @@ void Task::handle_app_id(const char *app_id)
void Task::handle_output_enter(struct wl_output *output) void Task::handle_output_enter(struct wl_output *output)
{ {
spdlog::debug("{} entered output {}", repr(), (void*)output);
if (ignored_) { if (ignored_) {
spdlog::debug("{} is ignored", repr()); spdlog::debug("{} is ignored", repr());
return; return;
} }
spdlog::debug("{} entered output {}", repr(), (void*)output);
if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) {
/* The task entered the output of the current bar make the button visible */ /* The task entered the output of the current bar make the button visible */
tbar_->add_button(button_); tbar_->add_button(button_);
@ -560,14 +597,17 @@ void Task::update()
{ {
bool markup = config_["markup"].isBool() ? config_["markup"].asBool() : false; bool markup = config_["markup"].isBool() ? config_["markup"].asBool() : false;
std::string title = title_; std::string title = title_;
std::string name = name_;
std::string app_id = app_id_; std::string app_id = app_id_;
if (markup) { if (markup) {
title = Glib::Markup::escape_text(title); title = Glib::Markup::escape_text(title);
name = Glib::Markup::escape_text(name);
app_id = Glib::Markup::escape_text(app_id); app_id = Glib::Markup::escape_text(app_id);
} }
if (!format_before_.empty()) { if (!format_before_.empty()) {
auto txt = fmt::format(format_before_, auto txt = fmt::format(format_before_,
fmt::arg("title", title), fmt::arg("title", title),
fmt::arg("name", name),
fmt::arg("app_id", app_id), fmt::arg("app_id", app_id),
fmt::arg("state", state_string()), fmt::arg("state", state_string()),
fmt::arg("short_state", state_string(true)) fmt::arg("short_state", state_string(true))
@ -581,6 +621,7 @@ void Task::update()
if (!format_after_.empty()) { if (!format_after_.empty()) {
auto txt = fmt::format(format_after_, auto txt = fmt::format(format_after_,
fmt::arg("title", title), fmt::arg("title", title),
fmt::arg("name", name),
fmt::arg("app_id", app_id), fmt::arg("app_id", app_id),
fmt::arg("state", state_string()), fmt::arg("state", state_string()),
fmt::arg("short_state", state_string(true)) fmt::arg("short_state", state_string(true))
@ -595,6 +636,7 @@ void Task::update()
if (!format_tooltip_.empty()) { if (!format_tooltip_.empty()) {
auto txt = fmt::format(format_tooltip_, auto txt = fmt::format(format_tooltip_,
fmt::arg("title", title), fmt::arg("title", title),
fmt::arg("name", name),
fmt::arg("app_id", app_id), fmt::arg("app_id", app_id),
fmt::arg("state", state_string()), fmt::arg("state", state_string()),
fmt::arg("short_state", state_string(true)) fmt::arg("short_state", state_string(true))
@ -722,6 +764,15 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu
} }
} }
// Load app_id remappings
if (config_["app_ids-mapping"].isObject()) {
const Json::Value& mapping = config_["app_ids-mapping"];
const std::vector<std::string> app_ids = config_["app_ids-mapping"].getMemberNames();
for (auto& app_id : app_ids) {
app_ids_replace_map_.emplace(app_id, mapping[app_id].asString());
}
}
icon_themes_.push_back(Gtk::IconTheme::get_default()); icon_themes_.push_back(Gtk::IconTheme::get_default());
} }
@ -853,10 +904,10 @@ bool Taskbar::all_outputs() const
return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); return config_["all-outputs"].isBool() && config_["all-outputs"].asBool();
} }
std::vector<Glib::RefPtr<Gtk::IconTheme>> Taskbar::icon_themes() const const std::vector<Glib::RefPtr<Gtk::IconTheme>>& Taskbar::icon_themes() const { return icon_themes_; }
{
return icon_themes_; const std::unordered_set<std::string>& Taskbar::ignore_list() const { return ignore_list_; }
}
const std::unordered_set<std::string> &Taskbar::ignore_list() const { return ignore_list_; } const std::map<std::string, std::string>& Taskbar::app_ids_replace_map() const { return app_ids_replace_map_; }
} /* namespace waybar::modules::wlr */ } /* namespace waybar::modules::wlr */

View File

@ -0,0 +1,472 @@
#include "modules/wlr/workspace_manager.hpp"
#include <gdk/gdkwayland.h>
#include <gtkmm.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <iterator>
#include <vector>
#include "gtkmm/widget.h"
#include "modules/wlr/workspace_manager_binding.hpp"
namespace waybar::modules::wlr {
uint32_t WorkspaceGroup::workspace_global_id = 0;
uint32_t WorkspaceManager::group_global_id = 0;
std::map<std::string, std::string> Workspace::icons_map_;
WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar,
const Json::Value &config)
: waybar::AModule(config, "workspaces", id, false, false),
bar_(bar),
box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) {
auto config_sort_by_name = config_["sort-by-name"];
if (config_sort_by_name.isBool()) {
sort_by_name_ = config_sort_by_name.asBool();
}
auto config_sort_by_coordinates = config_["sort-by-coordinates"];
if (config_sort_by_coordinates.isBool()) {
sort_by_coordinates_ = config_sort_by_coordinates.asBool();
}
auto config_all_outputs = config_["all-outputs"];
if (config_all_outputs.isBool()) {
all_outputs_ = config_all_outputs.asBool();
}
auto config_active_only = config_["active-only"];
if (config_active_only.isBool()) {
active_only_ = config_active_only.asBool();
creation_delayed_ = active_only_;
}
box_.set_name("workspaces");
if (!id.empty()) {
box_.get_style_context()->add_class(id);
}
event_box_.add(box_);
add_registry_listener(this);
if (!workspace_manager_) {
return;
}
}
auto WorkspaceManager::workspace_comparator() const
-> std::function<bool(std::unique_ptr<Workspace> &, std::unique_ptr<Workspace> &)> {
return [=](std::unique_ptr<Workspace> &lhs, std::unique_ptr<Workspace> &rhs) {
auto is_name_less = lhs->get_name() < rhs->get_name();
auto is_name_eq = lhs->get_name() == rhs->get_name();
auto is_coords_less = lhs->get_coords() < rhs->get_coords();
if (sort_by_name_) {
if (sort_by_coordinates_) {
return is_name_eq ? is_coords_less : is_name_less;
} else {
return is_name_less;
}
}
if (sort_by_coordinates_) {
return is_coords_less;
}
return lhs->id() < rhs->id();
};
}
auto WorkspaceManager::sort_workspaces() -> void {
std::vector<std::reference_wrapper<std::unique_ptr<Workspace>>> all_workspaces;
for (auto &group : groups_) {
auto &group_workspaces = group->workspaces();
all_workspaces.reserve(all_workspaces.size() +
std::distance(group_workspaces.begin(), group_workspaces.end()));
if (!active_only()) {
all_workspaces.insert(all_workspaces.end(), group_workspaces.begin(), group_workspaces.end());
continue;
}
for (auto &workspace : group_workspaces) {
if (!workspace->is_active()) {
continue;
}
all_workspaces.push_back(workspace);
}
}
std::sort(all_workspaces.begin(), all_workspaces.end(), workspace_comparator());
for (size_t i = 0; i < all_workspaces.size(); ++i) {
box_.reorder_child(all_workspaces[i].get()->get_button_ref(), i);
}
}
auto WorkspaceManager::register_manager(wl_registry *registry, uint32_t name, uint32_t version)
-> void {
if (workspace_manager_) {
spdlog::warn("Register workspace manager again although already registered!");
return;
}
if (version != 1) {
spdlog::warn("Using different workspace manager protocol version: {}", version);
}
workspace_manager_ = workspace_manager_bind(registry, name, version, this);
}
auto WorkspaceManager::handle_workspace_group_create(
zext_workspace_group_handle_v1 *workspace_group_handle) -> void {
auto new_id = ++group_global_id;
groups_.push_back(
std::make_unique<WorkspaceGroup>(bar_, box_, config_, *this, workspace_group_handle, new_id));
spdlog::debug("Workspace group {} created", new_id);
}
auto WorkspaceManager::handle_finished() -> void {
zext_workspace_manager_v1_destroy(workspace_manager_);
workspace_manager_ = nullptr;
}
auto WorkspaceManager::handle_done() -> void {
for (auto &group : groups_) {
group->handle_done();
}
dp.emit();
}
auto WorkspaceManager::update() -> void {
for (auto &group : groups_) {
group->update();
}
if (creation_delayed()) {
creation_delayed_ = false;
sort_workspaces();
}
AModule::update();
}
WorkspaceManager::~WorkspaceManager() {
if (!workspace_manager_) {
return;
}
zext_workspace_manager_v1_destroy(workspace_manager_);
workspace_manager_ = nullptr;
}
auto WorkspaceManager::remove_workspace_group(uint32_t id) -> void {
auto it = std::find_if(groups_.begin(),
groups_.end(),
[id](const std::unique_ptr<WorkspaceGroup> &g) { return g->id() == id; });
if (it == groups_.end()) {
spdlog::warn("Can't find group with id {}", id);
return;
}
groups_.erase(it);
}
auto WorkspaceManager::commit() -> void { zext_workspace_manager_v1_commit(workspace_manager_); }
WorkspaceGroup::WorkspaceGroup(const Bar &bar, Gtk::Box &box, const Json::Value &config,
WorkspaceManager &manager,
zext_workspace_group_handle_v1 *workspace_group_handle, uint32_t id)
: bar_(bar),
box_(box),
config_(config),
workspace_manager_(manager),
workspace_group_handle_(workspace_group_handle),
id_(id) {
add_workspace_group_listener(workspace_group_handle, this);
}
auto WorkspaceGroup::active_only() const -> bool { return workspace_manager_.active_only(); }
auto WorkspaceGroup::creation_delayed() const -> bool {
return workspace_manager_.creation_delayed();
}
auto WorkspaceGroup::add_button(Gtk::Button &button) -> void {
box_.pack_start(button, false, false);
}
WorkspaceGroup::~WorkspaceGroup() {
if (!workspace_group_handle_) {
return;
}
zext_workspace_group_handle_v1_destroy(workspace_group_handle_);
workspace_group_handle_ = nullptr;
}
auto WorkspaceGroup::handle_workspace_create(zext_workspace_handle_v1 *workspace) -> void {
auto new_id = ++workspace_global_id;
workspaces_.push_back(std::make_unique<Workspace>(bar_, config_, *this, workspace, new_id));
spdlog::debug("Workspace {} created", new_id);
}
auto WorkspaceGroup::handle_remove() -> void {
zext_workspace_group_handle_v1_destroy(workspace_group_handle_);
workspace_group_handle_ = nullptr;
workspace_manager_.remove_workspace_group(id_);
}
auto WorkspaceGroup::handle_output_enter(wl_output *output) -> void {
spdlog::debug("Output {} assigned to {} group", (void *)output, id_);
output_ = output;
if (!is_visible() || workspace_manager_.creation_delayed()) {
return;
}
for (auto &workspace : workspaces_) {
add_button(workspace->get_button_ref());
}
}
auto WorkspaceGroup::is_visible() const -> bool {
return output_ != nullptr &&
(workspace_manager_.all_outputs() ||
output_ == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()));
}
auto WorkspaceGroup::handle_output_leave() -> void {
spdlog::debug("Output {} remove from {} group", (void *)output_, id_);
output_ = nullptr;
if (output_ != gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj())) {
return;
}
for (auto &workspace : workspaces_) {
remove_button(workspace->get_button_ref());
}
}
auto WorkspaceGroup::update() -> void {
for (auto &workspace : workspaces_) {
if (workspace_manager_.creation_delayed()) {
add_button(workspace->get_button_ref());
if (is_visible() && (workspace->is_active() || workspace->is_urgent())) {
workspace->show();
}
}
workspace->update();
}
}
auto WorkspaceGroup::remove_workspace(uint32_t id) -> void {
auto it = std::find_if(workspaces_.begin(),
workspaces_.end(),
[id](const std::unique_ptr<Workspace> &w) { return w->id() == id; });
if (it == workspaces_.end()) {
spdlog::warn("Can't find workspace with id {}", id);
return;
}
workspaces_.erase(it);
}
auto WorkspaceGroup::handle_done() -> void {
need_to_sort = false;
if (!is_visible()) {
return;
}
for (auto &workspace : workspaces_) {
workspace->handle_done();
}
if (creation_delayed()) {
return;
}
if (!workspace_manager_.all_outputs()) {
sort_workspaces();
} else {
workspace_manager_.sort_workspaces();
}
}
auto WorkspaceGroup::commit() -> void { workspace_manager_.commit(); }
auto WorkspaceGroup::sort_workspaces() -> void {
std::sort(workspaces_.begin(), workspaces_.end(), workspace_manager_.workspace_comparator());
for (size_t i = 0; i < workspaces_.size(); ++i) {
box_.reorder_child(workspaces_[i]->get_button_ref(), i);
}
}
auto WorkspaceGroup::remove_button(Gtk::Button &button) -> void { box_.remove(button); }
Workspace::Workspace(const Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group,
zext_workspace_handle_v1 *workspace, uint32_t id)
: bar_(bar),
config_(config),
workspace_group_(workspace_group),
workspace_handle_(workspace),
id_(id) {
add_workspace_listener(workspace, this);
auto config_format = config["format"];
format_ = config_format.isString() ? config_format.asString() : "{name}";
with_icon_ = format_.find("{icon}") != std::string::npos;
if (with_icon_ && icons_map_.empty()) {
auto format_icons = config["format-icons"];
for (auto &name : format_icons.getMemberNames()) {
icons_map_.emplace(name, format_icons[name].asString());
}
}
/* Handle click events if configured */
if (config_["on-click"].isString() || config_["on-click-middle"].isString() ||
config_["on-click-right"].isString()) {
button_.add_events(Gdk::BUTTON_PRESS_MASK);
button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked),
false);
}
button_.set_relief(Gtk::RELIEF_NONE);
content_.set_center_widget(label_);
button_.add(content_);
if (!workspace_group.is_visible()) {
return;
}
workspace_group.add_button(button_);
button_.show_all();
}
Workspace::~Workspace() {
workspace_group_.remove_button(button_);
if (!workspace_handle_) {
return;
}
zext_workspace_handle_v1_destroy(workspace_handle_);
workspace_handle_ = nullptr;
}
auto Workspace::update() -> void {
label_.set_markup(fmt::format(
format_, fmt::arg("name", name_), fmt::arg("icon", with_icon_ ? get_icon() : "")));
}
auto Workspace::handle_state(const std::vector<uint32_t> &state) -> void {
state_ = 0;
for (auto state_entry : state) {
switch (state_entry) {
case ZEXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE:
state_ |= (uint32_t)State::ACTIVE;
break;
case ZEXT_WORKSPACE_HANDLE_V1_STATE_URGENT:
state_ |= (uint32_t)State::URGENT;
break;
case ZEXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN:
state_ |= (uint32_t)State::HIDDEN;
break;
}
}
}
auto Workspace::handle_remove() -> void {
zext_workspace_handle_v1_destroy(workspace_handle_);
workspace_handle_ = nullptr;
workspace_group_.remove_workspace(id_);
}
auto add_or_remove_class(Glib::RefPtr<Gtk::StyleContext> context, bool condition,
const std::string &class_name) {
if (condition) {
context->add_class(class_name);
} else {
context->remove_class(class_name);
}
}
auto Workspace::handle_done() -> void {
spdlog::debug("Workspace {} changed to state {}", id_, state_);
auto style_context = button_.get_style_context();
add_or_remove_class(style_context, is_active(), "active");
add_or_remove_class(style_context, is_urgent(), "urgent");
add_or_remove_class(style_context, is_hidden(), "hidden");
if (workspace_group_.creation_delayed()) {
return;
}
if (workspace_group_.active_only() && (is_active() || is_urgent())) {
button_.show_all();
} else if (workspace_group_.active_only() && !(is_active() || is_urgent())) {
button_.hide();
}
}
auto Workspace::get_icon() -> std::string {
if (is_active()) {
auto active_icon_it = icons_map_.find("active");
if (active_icon_it != icons_map_.end()) {
return active_icon_it->second;
}
}
auto named_icon_it = icons_map_.find(name_);
if (named_icon_it != icons_map_.end()) {
return named_icon_it->second;
}
auto default_icon_it = icons_map_.find("default");
if (default_icon_it != icons_map_.end()) {
return default_icon_it->second;
}
return name_;
}
auto Workspace::handle_clicked(GdkEventButton *bt) -> bool {
std::string action;
if (config_["on-click"].isString() && bt->button == 1) {
action = config_["on-click"].asString();
} else if (config_["on-click-middle"].isString() && bt->button == 2) {
action = config_["on-click-middle"].asString();
} else if (config_["on-click-right"].isString() && bt->button == 3) {
action = config_["on-click-right"].asString();
}
if (action.empty())
return true;
else if (action == "activate") {
zext_workspace_handle_v1_activate(workspace_handle_);
} else if (action == "close") {
zext_workspace_handle_v1_remove(workspace_handle_);
} else {
spdlog::warn("Unknown action {}", action);
}
workspace_group_.commit();
return true;
}
auto Workspace::show() -> void { button_.show_all(); }
auto Workspace::hide() -> void { button_.hide(); }
auto Workspace::handle_name(const std::string &name) -> void {
if (name_ != name) {
workspace_group_.set_need_to_sort();
}
name_ = name;
}
auto Workspace::handle_coordinates(const std::vector<uint32_t> &coordinates) -> void {
if (coordinates_ != coordinates) {
workspace_group_.set_need_to_sort();
}
coordinates_ = coordinates;
}
} // namespace waybar::modules::wlr

View File

@ -0,0 +1,135 @@
#include "modules/wlr/workspace_manager_binding.hpp"
#include <spdlog/spdlog.h>
#include <cstdint>
#include "client.hpp"
#include "modules/wlr/workspace_manager.hpp"
namespace waybar::modules::wlr {
static void handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface,
uint32_t version) {
if (std::strcmp(interface, zext_workspace_manager_v1_interface.name) == 0) {
static_cast<WorkspaceManager *>(data)->register_manager(registry, name, version);
}
}
static void handle_global_remove(void *data, wl_registry *registry, uint32_t name) {
/* Nothing to do here */
}
static const wl_registry_listener registry_listener_impl = {.global = handle_global,
.global_remove = handle_global_remove};
void add_registry_listener(void *data) {
wl_display * display = Client::inst()->wl_display;
wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener_impl, data);
wl_display_roundtrip(display);
wl_display_roundtrip(display);
}
static void workspace_manager_handle_workspace_group(
void *data, zext_workspace_manager_v1 *_, zext_workspace_group_handle_v1 *workspace_group) {
static_cast<WorkspaceManager *>(data)->handle_workspace_group_create(workspace_group);
}
static void workspace_manager_handle_done(void *data, zext_workspace_manager_v1 *_) {
static_cast<WorkspaceManager *>(data)->handle_done();
}
static void workspace_manager_handle_finished(void *data, zext_workspace_manager_v1 *_) {
static_cast<WorkspaceManager *>(data)->handle_finished();
}
static const zext_workspace_manager_v1_listener workspace_manager_impl = {
.workspace_group = workspace_manager_handle_workspace_group,
.done = workspace_manager_handle_done,
.finished = workspace_manager_handle_finished,
};
zext_workspace_manager_v1 *workspace_manager_bind(wl_registry *registry, uint32_t name,
uint32_t version, void *data) {
auto *workspace_manager = static_cast<zext_workspace_manager_v1 *>(
wl_registry_bind(registry, name, &zext_workspace_manager_v1_interface, version));
if (workspace_manager)
zext_workspace_manager_v1_add_listener(workspace_manager, &workspace_manager_impl, data);
else
spdlog::error("Failed to register manager");
return workspace_manager;
}
static void workspace_group_handle_output_enter(void *data, zext_workspace_group_handle_v1 *_,
wl_output *output) {
static_cast<WorkspaceGroup *>(data)->handle_output_enter(output);
}
static void workspace_group_handle_output_leave(void *data, zext_workspace_group_handle_v1 *_,
wl_output *output) {
static_cast<WorkspaceGroup *>(data)->handle_output_leave();
}
static void workspace_group_handle_workspace(void *data, zext_workspace_group_handle_v1 *_,
zext_workspace_handle_v1 *workspace) {
static_cast<WorkspaceGroup *>(data)->handle_workspace_create(workspace);
}
static void workspace_group_handle_remove(void *data, zext_workspace_group_handle_v1 *_) {
static_cast<WorkspaceGroup *>(data)->handle_remove();
}
static const zext_workspace_group_handle_v1_listener workspace_group_impl = {
.output_enter = workspace_group_handle_output_enter,
.output_leave = workspace_group_handle_output_leave,
.workspace = workspace_group_handle_workspace,
.remove = workspace_group_handle_remove};
void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle,
void * data) {
zext_workspace_group_handle_v1_add_listener(workspace_group_handle, &workspace_group_impl, data);
}
void workspace_handle_name(void *data, struct zext_workspace_handle_v1 *_, const char *name) {
static_cast<Workspace *>(data)->handle_name(name);
}
void workspace_handle_coordinates(void *data, struct zext_workspace_handle_v1 *_,
struct wl_array *coordinates) {
std::vector<uint32_t> coords_vec;
auto coords = static_cast<uint32_t *>(coordinates->data);
for (size_t i = 0; i < coordinates->size / sizeof(uint32_t); ++i) {
coords_vec.push_back(coords[i]);
}
static_cast<Workspace *>(data)->handle_coordinates(coords_vec);
}
void workspace_handle_state(void *data, struct zext_workspace_handle_v1 *workspace_handle,
struct wl_array *state) {
std::vector<uint32_t> state_vec;
auto states = static_cast<uint32_t *>(state->data);
for (size_t i = 0; i < state->size / sizeof(uint32_t); ++i) {
state_vec.push_back(states[i]);
}
static_cast<Workspace *>(data)->handle_state(state_vec);
}
void workspace_handle_remove(void *data, struct zext_workspace_handle_v1 *_) {
static_cast<Workspace *>(data)->handle_remove();
}
static const zext_workspace_handle_v1_listener workspace_impl = {
.name = workspace_handle_name,
.coordinates = workspace_handle_coordinates,
.state = workspace_handle_state,
.remove = workspace_handle_remove};
void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data) {
zext_workspace_handle_v1_add_listener(workspace_handle, &workspace_impl, data);
}
} // namespace waybar::modules::wlr

24
test/GlibTestsFixture.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <glibmm/main.h>
/**
* Minimal Glib application to be used for tests that require Glib main loop
*/
class GlibTestsFixture : public sigc::trackable {
public:
GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {}
void setTimeout(int timeout) {
Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); },
timeout);
}
void run(std::function<void()> fn) {
Glib::signal_idle().connect_once(fn);
main_loop_->run();
}
void quit() { main_loop_->quit(); }
protected:
Glib::RefPtr<Glib::MainLoop> main_loop_;
};

145
test/SafeSignal.cpp Normal file
View File

@ -0,0 +1,145 @@
#define CATCH_CONFIG_RUNNER
#include "util/SafeSignal.hpp"
#include <glibmm.h>
#include <catch2/catch.hpp>
#include <thread>
#include <type_traits>
#include "GlibTestsFixture.hpp"
using namespace waybar;
template <typename T>
using remove_cvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
/**
* Basic sanity test for SafeSignal:
* check that type deduction works, events are delivered and the order is right
* Running this with -fsanitize=thread should not fail
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal basic functionality", "[signal][thread][util]") {
const int NUM_EVENTS = 100;
int count = 0;
int last_value = 0;
SafeSignal<int, std::string> test_signal;
const auto main_tid = std::this_thread::get_id();
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto val, auto str) {
static_assert(std::is_same<int, decltype(val)>::value);
static_assert(std::is_same<std::string, decltype(str)>::value);
// check that we're in the same thread as the main loop
REQUIRE(std::this_thread::get_id() == main_tid);
// check event order
REQUIRE(val == last_value + 1);
last_value = val;
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
// check that events from the same thread are delivered and processed synchronously
test_signal.emit(1, "test");
REQUIRE(count == 1);
// start another thread and generate events
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
test_signal.emit(i, "test");
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}
template <typename T>
struct TestObject {
T value;
unsigned copied = 0;
unsigned moved = 0;
TestObject(const T& v) : value(v){};
~TestObject() = default;
TestObject(const TestObject& other)
: value(other.value), copied(other.copied + 1), moved(other.moved) {}
TestObject(TestObject&& other) noexcept
: value(std::move(other.value)),
copied(std::exchange(other.copied, 0)),
moved(std::exchange(other.moved, 0) + 1) {}
TestObject& operator=(const TestObject& other) {
value = other.value;
copied = other.copied + 1;
moved = other.moved;
return *this;
}
TestObject& operator=(TestObject&& other) noexcept {
value = std::move(other.value);
copied = std::exchange(other.copied, 0);
moved = std::exchange(other.moved, 0) + 1;
return *this;
}
bool operator==(T other) const { return value == other; }
operator T() const { return value; }
};
/*
* Check the number of copies/moves performed on the object passed through SafeSignal
*/
TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thread][util]") {
const int NUM_EVENTS = 3;
int count = 0;
SafeSignal<TestObject<int>> test_signal;
std::thread producer;
// timeout the test in 500ms
setTimeout(500);
test_signal.connect([&](auto& val) {
static_assert(std::is_same<TestObject<int>, remove_cvref_t<decltype(val)>>::value);
/* explicit move in the producer thread */
REQUIRE(val.moved <= 1);
/* copy within the SafeSignal queuing code */
REQUIRE(val.copied <= 1);
if (++count >= NUM_EVENTS) {
this->quit();
};
});
run([&]() {
test_signal.emit(1);
REQUIRE(count == 1);
producer = std::thread([&]() {
for (auto i = 2; i <= NUM_EVENTS; ++i) {
TestObject<int> t{i};
// check that signal.emit accepts moved objects
test_signal.emit(std::move(t));
}
});
});
producer.join();
REQUIRE(count == NUM_EVENTS);
}
int main(int argc, char* argv[]) {
Glib::init();
return Catch::Session().run(argc, argv);
}

View File

@ -2,6 +2,7 @@ test_inc = include_directories('../include')
test_dep = [ test_dep = [
catch2, catch2,
fmt, fmt,
gtkmm,
jsoncpp, jsoncpp,
spdlog, spdlog,
] ]
@ -14,8 +15,21 @@ config_test = executable(
include_directories: test_inc, include_directories: test_inc,
) )
safesignal_test = executable(
'safesignal_test',
'SafeSignal.cpp',
dependencies: test_dep,
include_directories: test_inc,
)
test( test(
'Configuration test', 'Configuration test',
config_test, config_test,
workdir: meson.source_root(), workdir: meson.source_root(),
) )
test(
'SafeSignal test',
safesignal_test,
workdir: meson.source_root(),
)