diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml
index 03e5d707..d5064fe4 100644
--- a/.github/workflows/freebsd.yml
+++ b/.github/workflows/freebsd.yml
@@ -13,6 +13,7 @@ jobs:
- name: Test in FreeBSD VM
uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0
with:
+ mem: 2048
usesh: true
prepare: |
export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio
diff --git a/README.md b/README.md
index 98b99a2d..587a5540 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
> Highly customizable Wayland bar for Sway and Wlroots based compositors.
> Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or
-[AUR](https://aur.archlinux.org/packages/waybar-git/), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)
+[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)
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)*
#### Current features
diff --git a/include/bar.hpp b/include/bar.hpp
index 6f3dfcf9..01a9d034 100644
--- a/include/bar.hpp
+++ b/include/bar.hpp
@@ -8,6 +8,9 @@
#include
#include
+#include
+#include
+
#include "AModule.hpp"
#include "xdg-output-unstable-v1-client-protocol.h"
@@ -36,6 +39,19 @@ struct bar_margins {
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 {
protected:
BarSurface() = default;
@@ -54,38 +70,56 @@ class BarSurface {
class Bar {
public:
+ using bar_mode_map = std::map;
+ static const bar_mode_map PRESET_MODES;
+ static const std::string_view MODE_DEFAULT;
+ static const std::string_view MODE_INVISIBLE;
+
Bar(struct waybar_output *w_output, const Json::Value &);
Bar(const Bar &) = delete;
- ~Bar() = default;
+ ~Bar();
+ void setMode(const std::string_view &);
void setVisible(bool visible);
void toggle();
void handleSignal(int);
struct waybar_output *output;
Json::Value config;
- struct wl_surface * surface;
- bool exclusive = true;
+ struct wl_surface *surface;
bool visible = true;
bool vertical = false;
Gtk::Window window;
+#ifdef HAVE_SWAY
+ std::string bar_id;
+#endif
+
private:
void onMap(GdkEventAny *);
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 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 surface_impl_;
- bar_layer layer_;
Gtk::Box left_;
Gtk::Box center_;
Gtk::Box right_;
Gtk::Box box_;
- std::vector> modules_left_;
- std::vector> modules_center_;
- std::vector> modules_right_;
+ std::vector> modules_left_;
+ std::vector> modules_center_;
+ std::vector> modules_right_;
+#ifdef HAVE_SWAY
+ using BarIpcClient = modules::sway::BarIpcClient;
+ std::unique_ptr _ipc_client;
+#endif
+ std::vector> modules_all_;
};
} // namespace waybar
diff --git a/include/client.hpp b/include/client.hpp
index bd80d0bd..7fc3dce7 100644
--- a/include/client.hpp
+++ b/include/client.hpp
@@ -29,6 +29,7 @@ class Client {
struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr;
std::vector> bars;
Config config;
+ std::string bar_id;
private:
Client() = default;
diff --git a/include/factory.hpp b/include/factory.hpp
index 43dd2cfd..3855ce22 100644
--- a/include/factory.hpp
+++ b/include/factory.hpp
@@ -51,6 +51,9 @@
#ifdef HAVE_LIBSNDIO
#include "modules/sndio.hpp"
#endif
+#ifdef HAVE_GIO_UNIX
+#include "modules/inhibitor.hpp"
+#endif
#include "bar.hpp"
#include "modules/custom.hpp"
#include "modules/temperature.hpp"
diff --git a/include/group.hpp b/include/group.hpp
new file mode 100644
index 00000000..f282f9c5
--- /dev/null
+++ b/include/group.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+#include
+#include
+#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
diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp
index 17752e4d..9f950192 100644
--- a/include/modules/clock.hpp
+++ b/include/modules/clock.hpp
@@ -17,6 +17,8 @@ struct waybar_time {
date::zoned_seconds ztime;
};
+const std::string kCalendarPlaceholder = "calendar";
+
class Clock : public ALabel {
public:
Clock(const std::string&, const Json::Value&);
@@ -26,18 +28,19 @@ class Clock : public ALabel {
private:
util::SleeperThread thread_;
std::locale locale_;
- const date::time_zone* time_zone_;
- bool fixed_time_zone_;
- int time_zone_idx_;
+ std::vector time_zones_;
+ int current_time_zone_idx_;
date::year_month_day cached_calendar_ymd_ = date::January/1/0;
std::string cached_calendar_text_;
+ bool is_calendar_in_tooltip_;
bool handleScroll(GdkEventScroll* e);
auto calendar_text(const waybar_time& wtime) -> std::string;
auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void;
auto first_day_of_week() -> date::weekday;
- bool setTimeZone(Json::Value zone_name);
+ const date::time_zone* current_timezone();
+ bool is_timezone_fixed();
};
} // namespace waybar::modules
diff --git a/include/modules/inhibitor.hpp b/include/modules/inhibitor.hpp
new file mode 100644
index 00000000..aa2f97d4
--- /dev/null
+++ b/include/modules/inhibitor.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+#include
+
+#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
diff --git a/include/modules/sway/bar.hpp b/include/modules/sway/bar.hpp
new file mode 100644
index 00000000..c4381a43
--- /dev/null
+++ b/include/modules/sway/bar.hpp
@@ -0,0 +1,49 @@
+#pragma once
+#include
+
+#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 signal_visible_;
+ SafeSignal signal_config_;
+};
+
+} // namespace modules::sway
+} // namespace waybar
diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp
index 1faf52b3..92e2bbaa 100644
--- a/include/modules/sway/language.hpp
+++ b/include/modules/sway/language.hpp
@@ -32,6 +32,7 @@ class Language : public ALabel, public sigc::trackable {
std::string short_name;
std::string variant;
std::string short_description;
+ std::string country_flag() const;
};
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_ACTIVE_LAYOUT_NAME_KEY;
-
+
Layout layout_;
std::string tooltip_format_ = "";
std::map layouts_map_;
diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp
new file mode 100644
index 00000000..3b68653c
--- /dev/null
+++ b/include/util/SafeSignal.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace waybar {
+
+/**
+ * Thread-safe signal wrapper.
+ * Uses Glib::Dispatcher to pass events to another thread and locked queue to pass the arguments.
+ */
+template
+struct SafeSignal : sigc::signal...)> {
+ public:
+ SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); }
+
+ template
+ 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(args)...);
+ } else {
+ {
+ std::unique_lock lock(mutex_);
+ queue_.emplace(std::forward(args)...);
+ }
+ dp_.emit();
+ }
+ }
+
+ template
+ inline void operator()(EmitArgs&&... args) {
+ emit(std::forward(args)...);
+ }
+
+ protected:
+ using signal_t = sigc::signal...)>;
+ using slot_t = decltype(std::declval().make_slot());
+ using arg_tuple_t = std::tuple...>;
+ // 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 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
diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd
new file mode 100644
index 00000000..0838f4d6
--- /dev/null
+++ b/man/waybar-inhibitor.5.scd
@@ -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": ""
+ }
+}
+```
diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd
index 92a647e6..1c88314c 100644
--- a/man/waybar-sway-language.5.scd
+++ b/man/waybar-sway-language.5.scd
@@ -37,6 +37,8 @@ Addressed by *sway/language*
*{variant}*: Variant of layout (e.g. "dvorak").
+*{flag}*: Country flag of layout.
+
# EXAMPLES
```
diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in
index 66d5b2eb..c42f6eb8 100644
--- a/man/waybar.5.scd.in
+++ b/man/waybar.5.scd.in
@@ -72,14 +72,19 @@ Also a minimal example configuration can be found on the at the bottom of this m
typeof: string ++
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* ++
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.
*passthrough* ++
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.
Intended to be used with either *top* or *overlay* layers and without exclusive zone.
@@ -89,6 +94,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.
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* ++
typeof: string|array ++
Paths to additional configuration files.
@@ -203,6 +218,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.
+## 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
- *waybar-backlight(5)*
diff --git a/meson.build b/meson.build
index 62ac8e36..cfe2d513 100644
--- a/meson.build
+++ b/meson.build
@@ -86,7 +86,7 @@ wayland_cursor = dependency('wayland-cursor')
wayland_protos = dependency('wayland-protocols')
gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0'])
dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk'))
-giounix = dependency('gio-unix-2.0', required: get_option('dbusmenu-gtk'))
+giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled()))
jsoncpp = dependency('jsoncpp')
sigcpp = dependency('sigc++-2.0')
libepoll = dependency('epoll-shim', required: false)
@@ -150,6 +150,7 @@ src_files = files(
'src/bar.cpp',
'src/client.cpp',
'src/config.cpp',
+ 'src/group.cpp',
'src/util/ustring_clen.cpp'
)
@@ -177,6 +178,7 @@ endif
add_project_arguments('-DHAVE_SWAY', language: 'cpp')
src_files += [
'src/modules/sway/ipc/client.cpp',
+ 'src/modules/sway/bar.cpp',
'src/modules/sway/mode.cpp',
'src/modules/sway/language.cpp',
'src/modules/sway/window.cpp',
@@ -240,6 +242,11 @@ if libsndio.found()
src_files += 'src/modules/sndio.cpp'
endif
+if (giounix.found() and not get_option('logind').disabled())
+ add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp')
+ src_files += 'src/modules/inhibitor.cpp'
+endif
+
if get_option('rfkill').enabled()
if is_linux
add_project_arguments('-DWANT_RFKILL', language: 'cpp')
@@ -257,6 +264,10 @@ else
src_files += 'src/modules/simpleclock.cpp'
endif
+if get_option('experimental')
+ add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp')
+endif
+
subdir('protocol')
executable(
@@ -341,6 +352,10 @@ if scdoc.found()
'waybar-sndio.5.scd',
]
+ if (giounix.found() and not get_option('logind').disabled())
+ man_files += 'waybar-inhibitor.5.scd'
+ endif
+
foreach file : man_files
path = '@0@'.format(file)
basename = path.split('/')[-1]
@@ -383,3 +398,4 @@ if clangtidy.found()
'-p', meson.build_root()
] + src_files)
endif
+
diff --git a/meson_options.txt b/meson_options.txt
index 81e44689..230a53d6 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -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('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL')
option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio')
+option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind')
option('tests', type: 'feature', value: 'auto', description: 'Enable tests')
+option('experimental', type : 'boolean', value : false, description: 'Enable experimental features')
diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py
index fa9aa58b..1630d97c 100755
--- a/resources/custom_modules/mediaplayer.py
+++ b/resources/custom_modules/mediaplayer.py
@@ -110,6 +110,7 @@ def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
for player in manager.props.player_names:
if arguments.player is not None and arguments.player != player.name:
diff --git a/src/bar.cpp b/src/bar.cpp
index a8b230e1..133c29aa 100644
--- a/src/bar.cpp
+++ b/src/bar.cpp
@@ -9,8 +9,13 @@
#include "bar.hpp"
#include "client.hpp"
#include "factory.hpp"
+#include "group.hpp"
#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+#ifdef HAVE_SWAY
+#include "modules/sway/bar.hpp"
+#endif
+
namespace waybar {
static constexpr const char* MIN_HEIGHT_MSG =
"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 =
"{} 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
+ * Assumes that all the values in the object are deserializable to the same type.
+ */
+template ::value>>
+void from_json(const Json::Value& j, std::map& 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
struct GLSSurfaceImpl : public BarSurface, public sigc::trackable {
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),
config(w_config),
window{Gtk::WindowType::WINDOW_TOPLEVEL},
- layer_{bar_layer::BOTTOM},
left_(Gtk::ORIENTATION_HORIZONTAL, 0),
center_(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["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();
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(window, *output);
}
- surface_impl_->setLayer(layer_);
- surface_impl_->setExclusiveZone(exclusive);
surface_impl_->setMargins(margins_);
- surface_impl_->setPassThrough(passthrough);
surface_impl_->setPosition(position);
surface_impl_->setSize(width, height);
+ /* 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));
+#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(*this);
+ } catch (const std::exception& exc) {
+ spdlog::warn("Failed to open bar ipc connection: {}", exc.what());
+ }
+ }
+#endif
+
setupWidgets();
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*) {
/*
* 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) {
visible = value;
- if (!visible) {
- window.get_style_context()->add_class("hidden");
- window.set_opacity(0);
- surface_impl_->setLayer(bar_layer::BOTTOM);
- } else {
- window.get_style_context()->remove_class("hidden");
- window.set_opacity(1);
- surface_impl_->setLayer(layer_);
- }
- surface_impl_->setExclusiveZone(exclusive && visible);
- surface_impl_->commit();
+ setMode(visible ? MODE_DEFAULT : MODE_INVISIBLE);
}
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) {
- for (auto& module : modules_left_) {
- auto* custom = dynamic_cast(module.get());
- if (custom != nullptr) {
- custom->refresh(signal);
- }
- }
- for (auto& module : modules_center_) {
- auto* custom = dynamic_cast(module.get());
- if (custom != nullptr) {
- custom->refresh(signal);
- }
- }
- for (auto& module : modules_right_) {
+ for (auto& module : modules_all_) {
auto* custom = dynamic_cast(module.get());
if (custom != nullptr) {
custom->refresh(signal);
@@ -614,19 +719,36 @@ void waybar::Bar::handleSignal(int signal) {
}
}
-void waybar::Bar::getModules(const Factory& factory, const std::string& pos) {
- if (config[pos].isArray()) {
- for (const auto& name : config[pos]) {
+void waybar::Bar::getModules(const Factory& factory, const std::string& pos, Gtk::Box* group = nullptr) {
+ auto module_list = group ? config[pos]["modules"] : config[pos];
+ if (module_list.isArray()) {
+ for (const auto& name : module_list) {
try {
- auto module = factory.makeModule(name.asString());
- if (pos == "modules-left") {
- modules_left_.emplace_back(module);
+ auto ref = name.asString();
+ AModule* 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);
- }
- if (pos == "modules-right") {
- modules_right_.emplace_back(module);
+
+ std::shared_ptr module_sp(module);
+ modules_all_.emplace_back(module_sp);
+ if (group) {
+ 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] {
try {
diff --git a/src/client.cpp b/src/client.cpp
index 95f5a295..8adbeac1 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -199,7 +199,6 @@ int waybar::Client::main(int argc, char *argv[]) {
bool show_version = false;
std::string config_opt;
std::string style_opt;
- std::string bar_id;
std::string log_level;
auto cli = clara::detail::Help(show_help) |
clara::detail::Opt(show_version)["-v"]["--version"]("Show version") |
diff --git a/src/factory.cpp b/src/factory.cpp
index a577751a..900653b5 100644
--- a/src/factory.cpp
+++ b/src/factory.cpp
@@ -30,10 +30,12 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
if (ref == "wlr/taskbar") {
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
#ifdef HAVE_RIVER
if (ref == "river/tags") {
return new waybar::modules::river::Tags(id, bar_, config_[name]);
@@ -92,6 +94,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const {
if (ref == "sndio") {
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
if (ref == "temperature") {
return new waybar::modules::Temperature(id, config_[name]);
diff --git a/src/group.cpp b/src/group.cpp
new file mode 100644
index 00000000..9d2188cc
--- /dev/null
+++ b/src/group.cpp
@@ -0,0 +1,19 @@
+#include "group.hpp"
+#include
+#include
+
+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
diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp
index 7c94c457..7e7d7420 100644
--- a/src/modules/clock.cpp
+++ b/src/modules/clock.cpp
@@ -14,17 +14,51 @@
using waybar::modules::waybar_time;
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()) {
- time_zone_idx_ = 0;
- setTimeZone(config_["timezones"][time_zone_idx_]);
- } else {
- setTimeZone(config_["timezone"]);
+ for (const auto& zone_name: config_["timezones"]) {
+ if (!zone_name.isString() || zone_name.asString().empty()) {
+ time_zones_.push_back(nullptr);
+ 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.");
}
+ // 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()) {
locale_ = std::locale(config_["locale"].asString());
} 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 {
- if (!fixed_time_zone_) {
- // Time zone can change. Be sure to pick that.
- time_zone_ = date::current_zone();
- }
-
- auto now = std::chrono::system_clock::now();
+ auto time_zone = current_timezone();
+ auto now = std::chrono::system_clock::now();
waybar_time wtime = {locale_,
- date::make_zoned(time_zone_, date::floor(now))};
-
- std::string text;
- if (!fixed_time_zone_) {
+ date::make_zoned(time_zone, date::floor(now))};
+ std::string text = "";
+ if (!is_timezone_fixed()) {
// As date dep is not fully compatible, prefer fmt
tzset();
auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now));
text = fmt::format(format_, localtime);
- label_.set_markup(text);
} else {
text = fmt::format(format_, wtime);
- label_.set_markup(text);
}
+ label_.set_markup(text);
if (tooltipEnabled()) {
if (config_["tooltip-format"].isString()) {
- const auto calendar = calendar_text(wtime);
- auto tooltip_format = config_["tooltip-format"].asString();
- auto tooltip_text = fmt::format(tooltip_format, wtime, fmt::arg("calendar", calendar));
- label_.set_tooltip_markup(tooltip_text);
- } else {
- label_.set_tooltip_markup(text);
+ std::string calendar_lines = "";
+ if (is_calendar_in_tooltip_) {
+ calendar_lines = calendar_text(wtime);
+ }
+ auto tooltip_format = config_["tooltip-format"].asString();
+ text = fmt::format(tooltip_format, wtime, fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines));
}
}
+
+ label_.set_tooltip_markup(text);
// Call parent 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) {
// defer to user commands if set
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) {
return true;
}
- if (!config_["timezones"].isArray() || config_["timezones"].empty()) {
+ if (time_zones_.size() == 1) {
return true;
}
- auto nr_zones = config_["timezones"].size();
+
+ auto nr_zones = time_zones_.size();
if (dir == SCROLL_DIR::UP) {
- size_t new_idx = time_zone_idx_ + 1;
- time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx;
+ size_t new_idx = current_time_zone_idx_ + 1;
+ current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx;
} else {
- time_zone_idx_ = time_zone_idx_ == 0 ? nr_zones - 1 : time_zone_idx_ - 1;
+ current_time_zone_idx_ = current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1;
}
- setTimeZone(config_["timezones"][time_zone_idx_]);
+
update();
return true;
}
diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp
new file mode 100644
index 00000000..1e3f2d35
--- /dev/null
+++ b/src/modules/inhibitor.cpp
@@ -0,0 +1,175 @@
+#include "modules/inhibitor.hpp"
+
+#include
+#include
+#include
+
+namespace {
+
+using DBus = std::unique_ptr;
+
+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
diff --git a/src/modules/network.cpp b/src/modules/network.cpp
index e7b20ab5..b86989f3 100644
--- a/src/modules/network.cpp
+++ b/src/modules/network.cpp
@@ -789,8 +789,8 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) {
// signalstrength in dBm from mBm
signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100;
- // WiFi-hardware usually operates in the range -90 to -20dBm.
- const int hardwareMax = -20;
+ // WiFi-hardware usually operates in the range -90 to -30dBm.
+ const int hardwareMax = -30;
const int hardwareMin = -90;
const int strength =
((signal_strength_dbm_ - hardwareMin) / double{hardwareMax - hardwareMin}) * 100;
diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp
index cf427800..3c320608 100644
--- a/src/modules/pulseaudio.cpp
+++ b/src/modules/pulseaudio.cpp
@@ -79,6 +79,13 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) {
if (dir == SCROLL_DIR::NONE) {
return true;
}
+ if (config_["reverse-scrolling"].asInt() == 1){
+ if (dir == SCROLL_DIR::UP) {
+ dir = SCROLL_DIR::DOWN;
+ } else if (dir == SCROLL_DIR::DOWN) {
+ dir = SCROLL_DIR::UP;
+ }
+ }
double volume_tick = static_cast(PA_VOLUME_NORM) / 100;
pa_volume_t change = volume_tick;
pa_cvolume pa_volume = pa_volume_;
@@ -211,7 +218,7 @@ static const std::array ports = {
};
const std::vector waybar::modules::Pulseaudio::getPulseIcon() const {
- std::vector res = {default_source_name_};
+ std::vector res = {current_sink_name_, default_source_name_};
std::string nameLC = port_name_ + form_factor_;
std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower);
for (auto const &port : ports) {
diff --git a/src/modules/sway/bar.cpp b/src/modules/sway/bar.cpp
new file mode 100644
index 00000000..6ad74af1
--- /dev/null
+++ b/src/modules/sway/bar.cpp
@@ -0,0 +1,107 @@
+#include "modules/sway/bar.hpp"
+
+#include
+#include
+
+#include
+
+#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
diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp
index 186fa4bb..73a64c3b 100644
--- a/src/modules/sway/language.cpp
+++ b/src/modules/sway/language.cpp
@@ -99,7 +99,8 @@ auto Language::update() -> void {
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
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);
if (tooltipEnabled()) {
if (tooltip_format_ != "") {
@@ -107,7 +108,8 @@ auto Language::update() -> void {
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
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);
} else {
label_.set_tooltip_markup(display_layout);
@@ -212,4 +214,15 @@ Language::XKBContext::~XKBContext() {
rxkb_context_unref(context_);
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(result)};
+}
} // namespace waybar::modules::sway
diff --git a/test/GlibTestsFixture.hpp b/test/GlibTestsFixture.hpp
new file mode 100644
index 00000000..a21c8e07
--- /dev/null
+++ b/test/GlibTestsFixture.hpp
@@ -0,0 +1,24 @@
+#pragma once
+#include
+/**
+ * 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 fn) {
+ Glib::signal_idle().connect_once(fn);
+ main_loop_->run();
+ }
+
+ void quit() { main_loop_->quit(); }
+
+ protected:
+ Glib::RefPtr main_loop_;
+};
diff --git a/test/SafeSignal.cpp b/test/SafeSignal.cpp
new file mode 100644
index 00000000..2c67317b
--- /dev/null
+++ b/test/SafeSignal.cpp
@@ -0,0 +1,145 @@
+#define CATCH_CONFIG_RUNNER
+#include "util/SafeSignal.hpp"
+
+#include
+
+#include
+#include
+#include
+
+#include "GlibTestsFixture.hpp"
+
+using namespace waybar;
+
+template
+using remove_cvref_t = typename std::remove_cv::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 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::value);
+ static_assert(std::is_same::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
+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> test_signal;
+
+ std::thread producer;
+
+ // timeout the test in 500ms
+ setTimeout(500);
+
+ test_signal.connect([&](auto& val) {
+ static_assert(std::is_same, remove_cvref_t>::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 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);
+}
diff --git a/test/meson.build b/test/meson.build
index 85b9771f..bbef21e7 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -2,6 +2,7 @@ test_inc = include_directories('../include')
test_dep = [
catch2,
fmt,
+ gtkmm,
jsoncpp,
spdlog,
]
@@ -14,8 +15,21 @@ config_test = executable(
include_directories: test_inc,
)
+safesignal_test = executable(
+ 'safesignal_test',
+ 'SafeSignal.cpp',
+ dependencies: test_dep,
+ include_directories: test_inc,
+)
+
test(
'Configuration test',
config_test,
workdir: meson.source_root(),
)
+
+test(
+ 'SafeSignal test',
+ safesignal_test,
+ workdir: meson.source_root(),
+)