diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48a0e9b..6248817 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,8 @@
* `online` tag to the `alsa` module.
* alsa: `volume` and `muted` options, allowing you to configure which
channels to use as source for the volume level and muted state.
+* foreign-toplevel: Wayland module that provides information about
+ currently opened windows.
### Changed
diff --git a/doc/meson.build b/doc/meson.build
index 4598ab2..a1550c4 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -6,6 +6,7 @@ scdoc_prog = find_program(scdoc.get_pkgconfig_variable('scdoc'), native: true)
foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd',
'yambar-modules-alsa.5.scd', 'yambar-modules-backlight.5.scd',
'yambar-modules-battery.5.scd', 'yambar-modules-clock.5.scd',
+ 'yambar-modules-foreign-toplevel.5.scd',
'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd',
'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd',
'yambar-modules-removables.5.scd', 'yambar-modules-river.5.scd',
diff --git a/doc/yambar-modules-foreign-toplevel.5.scd b/doc/yambar-modules-foreign-toplevel.5.scd
new file mode 100644
index 0000000..6a65ec3
--- /dev/null
+++ b/doc/yambar-modules-foreign-toplevel.5.scd
@@ -0,0 +1,79 @@
+yambar-modules-foreign-toplevel(5)
+
+# NAME
+foreign-toplevel - This module provides information about toplevel windows in Wayland
+
+# DESCRIPTION
+
+This module uses the _wlr foreign toplevel management_ Wayland
+protocol to provide information about currently open windows, such as
+their application ID, window title, and their current state
+(maximized/minimized/fullscreen/activated).
+
+The configuration for the foreign-toplevel module specifies a
+_template_ particle. This particle will be instantiated once for each
+window.
+
+Note: Wayland only.
+
+
+# TAGS
+
+[[ *Name*
+:[ *Type*
+:[ *Description*
+| app-id
+: string
+: The application ID (typically the application name)
+| title
+: string
+: The window title
+| maximized
+: bool
+: True if the window is currently maximized
+| minimized
+: bool
+: True if the window is currently minimized
+| fullscreen
+: bool
+: True if the window is currently fullscreened
+| activated
+: bool
+: True if the window is currently activated (i.e. has focus)
+
+
+# CONFIGURATION
+
+[[ *Name*
+:[ *Type*
+:[ *Req*
+:[ *Description*
+| content
+: particle
+: yes
+: Template particle that will be instantiated once for each window
+| all-monitors
+: bool
+: no
+: When set to true, only windows on the same monitor the bar will be
+ used. The default is false.
+
+
+# EXAMPLES
+
+```
+bar:
+ left:
+ - foreign-toplevel:
+ content:
+ map:
+ tag: activated
+ values:
+ false: {empty: {}}
+ true:
+ - string: {text: "{app-id}: {title}"}
+```
+
+# SEE ALSO
+
+*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5)
diff --git a/doc/yambar-modules-river.5.scd b/doc/yambar-modules-river.5.scd
index 341176e..c9327d5 100644
--- a/doc/yambar-modules-river.5.scd
+++ b/doc/yambar-modules-river.5.scd
@@ -1,7 +1,7 @@
yambar-modules-river(5)
# NAME
-river - This module provide information about the river tags
+river - This module provides information about the river tags
# DESCRIPTION
diff --git a/external/wlr-foreign-toplevel-management-unstable-v1.xml b/external/wlr-foreign-toplevel-management-unstable-v1.xml
new file mode 100644
index 0000000..1081337
--- /dev/null
+++ b/external/wlr-foreign-toplevel-management-unstable-v1.xml
@@ -0,0 +1,270 @@
+
+
+
+ Copyright © 2018 Ilia Bozhinov
+
+ Permission to use, copy, modify, distribute, and sell this
+ software and its documentation for any purpose is hereby granted
+ without fee, provided that the above copyright notice appear in
+ all copies and that both that copyright notice and this permission
+ notice appear in supporting documentation, and that the name of
+ the copyright holders not be used in advertising or publicity
+ pertaining to distribution of the software without specific,
+ written prior permission. The copyright holders make no
+ representations about the suitability of this software for any
+ purpose. It is provided "as is" without express or implied
+ warranty.
+
+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ THIS SOFTWARE.
+
+
+
+
+ The purpose of this protocol is to enable the creation of taskbars
+ and docks by providing them with a list of opened applications and
+ letting them request certain actions on them, like maximizing, etc.
+
+ After a client binds the zwlr_foreign_toplevel_manager_v1, each opened
+ toplevel window will be sent via the toplevel event
+
+
+
+
+ This event is emitted whenever a new toplevel window is created. It
+ is emitted for all toplevels, regardless of the app that has created
+ them.
+
+ All initial details of the toplevel(title, app_id, states, etc.) will
+ be sent immediately after this event via the corresponding events in
+ zwlr_foreign_toplevel_handle_v1.
+
+
+
+
+
+
+ Indicates the client no longer wishes to receive events for new toplevels.
+ However the compositor may emit further toplevel_created events, until
+ the finished event is emitted.
+
+ The client must not send any more requests after this one.
+
+
+
+
+
+ This event indicates that the compositor is done sending events to the
+ zwlr_foreign_toplevel_manager_v1. The server will destroy the object
+ immediately after sending this request, so it will become invalid and
+ the client should free any resources associated with it.
+
+
+
+
+
+
+ A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
+ window. Each app may have multiple opened toplevels.
+
+ Each toplevel has a list of outputs it is visible on, conveyed to the
+ client with the output_enter and output_leave events.
+
+
+
+
+ This event is emitted whenever the title of the toplevel changes.
+
+
+
+
+
+
+ This event is emitted whenever the app-id of the toplevel changes.
+
+
+
+
+
+
+ This event is emitted whenever the toplevel becomes visible on
+ the given output. A toplevel may be visible on multiple outputs.
+
+
+
+
+
+
+ This event is emitted whenever the toplevel stops being visible on
+ the given output. It is guaranteed that an entered-output event
+ with the same output has been emitted before this event.
+
+
+
+
+
+
+ Requests that the toplevel be maximized. If the maximized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be unmaximized. If the maximized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be minimized. If the minimized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Requests that the toplevel be unminimized. If the minimized state actually
+ changes, this will be indicated by the state event.
+
+
+
+
+
+ Request that this toplevel be activated on the given seat.
+ There is no guarantee the toplevel will be actually activated.
+
+
+
+
+
+
+ The different states that a toplevel can have. These have the same meaning
+ as the states with the same names defined in xdg-toplevel
+
+
+
+
+
+
+
+
+
+
+ This event is emitted immediately after the zlw_foreign_toplevel_handle_v1
+ is created and each time the toplevel state changes, either because of a
+ compositor action or because of a request in this protocol.
+
+
+
+
+
+
+
+ This event is sent after all changes in the toplevel state have been
+ sent.
+
+ This allows changes to the zwlr_foreign_toplevel_handle_v1 properties
+ to be seen as atomic, even if they happen via multiple events.
+
+
+
+
+
+ Send a request to the toplevel to close itself. The compositor would
+ typically use a shell-specific method to carry out this request, for
+ example by sending the xdg_toplevel.close event. However, this gives
+ no guarantees the toplevel will actually be destroyed. If and when
+ this happens, the zwlr_foreign_toplevel_handle_v1.closed event will
+ be emitted.
+
+
+
+
+
+ The rectangle of the surface specified in this request corresponds to
+ the place where the app using this protocol represents the given toplevel.
+ It can be used by the compositor as a hint for some operations, e.g
+ minimizing. The client is however not required to set this, in which
+ case the compositor is free to decide some default value.
+
+ If the client specifies more than one rectangle, only the last one is
+ considered.
+
+ The dimensions are given in surface-local coordinates.
+ Setting width=height=0 removes the already-set rectangle.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This event means the toplevel has been destroyed. It is guaranteed there
+ won't be any more events for this zwlr_foreign_toplevel_handle_v1. The
+ toplevel itself becomes inert so any requests will be ignored except the
+ destroy request.
+
+
+
+
+
+ Destroys the zwlr_foreign_toplevel_handle_v1 object.
+
+ This request should be called either when the client does not want to
+ use the toplevel anymore or after the closed event to finalize the
+ destruction of the object.
+
+
+
+
+
+
+
+ Requests that the toplevel be fullscreened on the given output. If the
+ fullscreen state and/or the outputs the toplevel is visible on actually
+ change, this will be indicated by the state and output_enter/leave
+ events.
+
+ The output parameter is only a hint to the compositor. Also, if output
+ is NULL, the compositor should decide which output the toplevel will be
+ fullscreened on, if at all.
+
+
+
+
+
+
+ Requests that the toplevel be unfullscreened. If the fullscreen state
+ actually changes, this will be indicated by the state event.
+
+
+
+
+
+
+
+ This event is emitted whenever the parent of the toplevel changes.
+
+ No event is emitted when the parent handle is destroyed by the client.
+
+
+
+
+
diff --git a/modules/foreign-toplevel.c b/modules/foreign-toplevel.c
new file mode 100644
index 0000000..e71e825
--- /dev/null
+++ b/modules/foreign-toplevel.c
@@ -0,0 +1,672 @@
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#define LOG_MODULE "foreign-toplevel"
+#define LOG_ENABLE_DBG 1
+#include "../log.h"
+#include "../plugin.h"
+#include "../particles/dynlist.h"
+
+#include "wlr-foreign-toplevel-management-unstable-v1.h"
+#include "xdg-output-unstable-v1.h"
+
+#define min(x, y) ((x) < (y) ? (x) : (y))
+
+static const int required_manager_interface_version = 2;
+
+struct output {
+ struct module *mod;
+
+ uint32_t wl_name;
+ struct wl_output *wl_output;
+ struct zxdg_output_v1 *xdg_output;
+
+ char *name;
+};
+
+struct toplevel {
+ struct module *mod;
+ struct zwlr_foreign_toplevel_handle_v1 *handle;
+
+ char *app_id;
+ char *title;
+
+ bool maximized;
+ bool minimized;
+ bool activated;
+ bool fullscreen;
+
+ tll(const struct output *) outputs;
+};
+
+struct private {
+ struct particle *template;
+ uint32_t manager_wl_name;
+ struct zwlr_foreign_toplevel_manager_v1 *manager;
+ struct zxdg_output_manager_v1 *xdg_output_manager;
+
+ bool all_monitors;
+ tll(struct toplevel) toplevels;
+ tll(struct output) outputs;
+};
+
+static void
+output_free(struct output *output)
+{
+ free(output->name);
+ if (output->xdg_output != NULL)
+ zxdg_output_v1_destroy(output->xdg_output);
+ if (output->wl_output != NULL)
+ wl_output_release(output->wl_output);
+}
+
+static void
+toplevel_free(struct toplevel *top)
+{
+ if (top->handle != NULL)
+ zwlr_foreign_toplevel_handle_v1_destroy(top->handle);
+
+ free(top->app_id);
+ free(top->title);
+ tll_free(top->outputs);
+}
+
+static void
+destroy(struct module *mod)
+{
+ struct private *m = mod->private;
+ m->template->destroy(m->template);
+
+ assert(tll_length(m->toplevels) == 0);
+ assert(tll_length(m->outputs) == 0);
+
+ free(m);
+ module_default_destroy(mod);
+}
+
+static const char *
+description(struct module *mod)
+{
+ return "toplevel";
+}
+
+static struct exposable *
+content(struct module *mod)
+{
+ const struct private *m = mod->private;
+
+ mtx_lock(&mod->lock);
+
+ const size_t toplevel_count = tll_length(m->toplevels);
+ size_t show_count = 0;
+ struct exposable *toplevels[toplevel_count];
+
+ const char *current_output = mod->bar->output_name(mod->bar);
+
+ tll_foreach(m->toplevels, it) {
+ const struct toplevel *top = &it->item;
+
+ bool show = false;
+
+ if (m->all_monitors)
+ show = true;
+ else if (current_output != NULL) {
+ tll_foreach(top->outputs, it2) {
+ const struct output *output = it2->item;
+ if (output->name != NULL &&
+ strcmp(output->name, current_output) == 0)
+ {
+ show = true;
+ break;
+ }
+ }
+ }
+
+ if (!show)
+ continue;
+
+ struct tag_set tags = {
+ .tags = (struct tag *[]){
+ tag_new_string(mod, "app-id", it->item.app_id),
+ tag_new_string(mod, "title", it->item.title),
+ tag_new_bool(mod, "maximized", it->item.maximized),
+ tag_new_bool(mod, "minimized", it->item.minimized),
+ tag_new_bool(mod, "activated", it->item.activated),
+ tag_new_bool(mod, "fullscreen", it->item.fullscreen),
+ },
+ .count = 6,
+ };
+
+ toplevels[show_count++] = m->template->instantiate(m->template, &tags);
+ tag_set_destroy(&tags);
+ }
+
+ mtx_unlock(&mod->lock);
+ return dynlist_exposable_new(toplevels, show_count, 0, 0);
+}
+
+static bool
+verify_iface_version(const char *iface, uint32_t version, uint32_t wanted)
+{
+ if (version >= wanted)
+ return true;
+
+ LOG_ERR("%s: need interface version %u, but compositor only implements %u",
+ iface, wanted, version);
+
+ return false;
+}
+
+static void
+xdg_output_handle_logical_position(void *data,
+ struct zxdg_output_v1 *xdg_output,
+ int32_t x, int32_t y)
+{
+}
+
+static void
+xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output,
+ int32_t width, int32_t height)
+{
+}
+
+static void
+xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output)
+{
+}
+
+static void
+xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output,
+ const char *name)
+{
+ struct output *output = data;
+ struct module *mod = output->mod;
+
+ mtx_lock(&mod->lock);
+ {
+ free(output->name);
+ output->name = name != NULL ? strdup(name) : NULL;
+ }
+ mtx_unlock(&mod->lock);
+}
+
+static void
+xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output,
+ const char *description)
+{
+}
+
+static struct zxdg_output_v1_listener xdg_output_listener = {
+ .logical_position = xdg_output_handle_logical_position,
+ .logical_size = xdg_output_handle_logical_size,
+ .done = xdg_output_handle_done,
+ .name = xdg_output_handle_name,
+ .description = xdg_output_handle_description,
+};
+
+static void
+title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title)
+{
+ struct toplevel *top = data;
+
+ mtx_lock(&top->mod->lock);
+ {
+ free(top->title);
+ top->title = title != NULL ? strdup(title) : NULL;
+ }
+ mtx_unlock(&top->mod->lock);
+}
+
+static void
+app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id)
+{
+ struct toplevel *top = data;
+
+ mtx_lock(&top->mod->lock);
+ {
+ free(top->app_id);
+ top->app_id = app_id != NULL ? strdup(app_id) : NULL;
+ }
+ mtx_unlock(&top->mod->lock);
+}
+
+static void
+output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle,
+ struct wl_output *wl_output)
+{
+ struct toplevel *top = data;
+ struct module *mod = top->mod;
+ struct private *m = mod->private;
+
+ mtx_lock(&mod->lock);
+
+ const struct output *output = NULL;
+ tll_foreach(m->outputs, it) {
+ if (it->item.wl_output == wl_output) {
+ output = &it->item;
+ break;
+ }
+ }
+
+ if (output == NULL) {
+ LOG_ERR("output-enter event on untracked output");
+ goto out;
+ }
+
+ tll_foreach(top->outputs, it) {
+ if (it->item == output) {
+ LOG_ERR("output-enter event on output we're already on");
+ goto out;
+ }
+ }
+
+ LOG_DBG("mapped: %s:%s on %s", top->app_id, top->title, output->name);
+ tll_push_back(top->outputs, output);
+
+out:
+ mtx_unlock(&mod->lock);
+}
+
+static void
+output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle,
+ struct wl_output *wl_output)
+{
+ struct toplevel *top = data;
+ struct module *mod = top->mod;
+ struct private *m = mod->private;
+
+ mtx_lock(&mod->lock);
+
+ const struct output *output = NULL;
+ tll_foreach(m->outputs, it) {
+ if (it->item.wl_output == wl_output) {
+ output = &it->item;
+ break;
+ }
+ }
+
+ if (output == NULL) {
+ LOG_ERR("output-leave event on untracked output");
+ goto out;
+ }
+
+ bool output_removed = false;
+ tll_foreach(top->outputs, it) {
+ if (it->item == output) {
+ LOG_DBG("unmapped: %s:%s from %s",
+ top->app_id, top->title, output->name);
+ tll_remove(top->outputs, it);
+ output_removed = true;
+ break;
+ }
+ }
+
+ if (!output_removed) {
+ LOG_ERR("output-leave event on an output we're not on");
+ goto out;
+ }
+
+out:
+ mtx_unlock(&mod->lock);
+}
+
+static void
+state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle,
+ struct wl_array *states)
+{
+ struct toplevel *top = data;
+
+ bool maximized = false;
+ bool minimized = false;
+ bool activated = false;
+ bool fullscreen = false;
+
+ enum zwlr_foreign_toplevel_handle_v1_state *state;
+ wl_array_for_each(state, states) {
+ switch (*state) {
+ case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: maximized = true; break;
+ case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: minimized = true; break;
+ case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: activated = true; break;
+ case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: fullscreen = true; break;
+ }
+ }
+
+ mtx_lock(&top->mod->lock);
+ {
+ top->maximized = maximized;
+ top->minimized = minimized;
+ top->activated = activated;
+ top->fullscreen = fullscreen;
+ }
+ mtx_unlock(&top->mod->lock);
+}
+
+static void
+done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle)
+{
+ struct toplevel *top = data;
+ const struct bar *bar = top->mod->bar;
+ bar->refresh(bar);
+}
+
+static void
+closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle)
+{
+ struct toplevel *top = data;
+ struct module *mod = top->mod;
+ struct private *m = mod->private;
+
+ mtx_lock(&mod->lock);
+ tll_foreach(m->toplevels, it) {
+ if (it->item.handle == handle) {
+ toplevel_free(top);
+ tll_remove(m->toplevels, it);
+ break;
+ }
+ }
+ mtx_unlock(&mod->lock);
+}
+
+static void
+parent(void *data,
+ struct zwlr_foreign_toplevel_handle_v1 *handle,
+ struct zwlr_foreign_toplevel_handle_v1 *parent)
+{
+}
+
+static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_listener = {
+ .title = &title,
+ .app_id = &app_id,
+ .output_enter = &output_enter,
+ .output_leave = &output_leave,
+ .state = &state,
+ .done = &done,
+ .closed = &closed,
+ .parent = &parent,
+};
+
+static void
+toplevel(void *data,
+ struct zwlr_foreign_toplevel_manager_v1 *manager,
+ struct zwlr_foreign_toplevel_handle_v1 *handle)
+{
+ struct module *mod = data;
+ struct private *m = mod->private;
+
+ struct toplevel toplevel = {
+ .mod = mod,
+ .handle = handle,
+ };
+
+ mtx_lock(&mod->lock);
+ {
+ tll_push_back(m->toplevels, toplevel);
+
+ zwlr_foreign_toplevel_handle_v1_add_listener(
+ handle, &toplevel_listener, &tll_back(m->toplevels));
+ }
+ mtx_unlock(&mod->lock);
+}
+
+static void
+finished(void *data,
+ struct zwlr_foreign_toplevel_manager_v1 *manager)
+{
+ struct module *mod = data;
+ struct private *m = mod->private;
+
+ assert(m->manager == manager);
+ zwlr_foreign_toplevel_manager_v1_destroy(m->manager);
+ m->manager = NULL;
+}
+
+static const struct zwlr_foreign_toplevel_manager_v1_listener manager_listener = {
+ .toplevel = &toplevel,
+ .finished = &finished,
+};
+
+static void
+output_xdg_output(struct output *output)
+{
+ struct private *m = output->mod->private;
+
+ if (m->xdg_output_manager == NULL)
+ return;
+ if (output->xdg_output != NULL)
+ return;
+
+ output->xdg_output = zxdg_output_manager_v1_get_xdg_output(
+ m->xdg_output_manager, output->wl_output);
+ zxdg_output_v1_add_listener(
+ output->xdg_output, &xdg_output_listener, output);
+}
+
+static void
+handle_global(void *data, struct wl_registry *registry,
+ uint32_t name, const char *interface, uint32_t version)
+{
+ struct module *mod = data;
+ struct private *m = mod->private;
+
+ if (strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) {
+ if (!verify_iface_version(interface, version, required_manager_interface_version))
+ return;
+
+ m->manager_wl_name = name;
+ }
+
+ else if (strcmp(interface, wl_output_interface.name) == 0) {
+ const uint32_t required = 3;
+ if (!verify_iface_version(interface, version, required))
+ return;
+
+ struct output output = {
+ .mod = mod,
+ .wl_name = name,
+ .wl_output = wl_registry_bind(
+ registry, name, &wl_output_interface, required),
+ };
+
+ mtx_lock(&mod->lock);
+ tll_push_back(m->outputs, output);
+ output_xdg_output(&tll_back(m->outputs));
+ mtx_unlock(&mod->lock);
+ }
+
+ else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
+ const uint32_t required = 2;
+ if (!verify_iface_version(interface, version, required))
+ return;
+
+ m->xdg_output_manager = wl_registry_bind(
+ registry, name, &zxdg_output_manager_v1_interface, required);
+
+ mtx_lock(&mod->lock);
+ tll_foreach(m->outputs, it)
+ output_xdg_output(&it->item);
+ mtx_unlock(&mod->lock);
+ }
+}
+
+static void
+handle_global_remove(void *data, struct wl_registry *registry, uint32_t name)
+{
+ struct module *mod = data;
+ struct private *m = mod->private;
+
+ mtx_lock(&mod->lock);
+
+ tll_foreach(m->outputs, it) {
+ const struct output *output = &it->item;
+ if (output->wl_name == name) {
+
+ /* Loop all toplevels */
+ tll_foreach(m->toplevels, it2) {
+
+ /* And remove this output from their list of tracked
+ * outputs */
+ tll_foreach(it2->item.outputs, it3) {
+ if (it3->item == output) {
+ tll_remove(it2->item.outputs, it3);
+ break;
+ }
+ }
+ }
+
+ tll_remove(m->outputs, it);
+ goto out;
+ }
+ }
+
+out:
+ mtx_unlock(&mod->lock);
+}
+
+static const struct wl_registry_listener registry_listener = {
+ .global = &handle_global,
+ .global_remove = &handle_global_remove,
+};
+
+static int
+run(struct module *mod)
+{
+ struct private *m = mod->private;
+ int ret = -1;
+
+ struct wl_display *display = NULL;
+ struct wl_registry *registry = NULL;
+
+ if ((display = wl_display_connect(NULL)) == NULL) {
+ LOG_ERR("no Wayland compositor running");
+ goto out;
+ }
+
+ if ((registry = wl_display_get_registry(display)) == NULL ||
+ wl_registry_add_listener(registry, ®istry_listener, mod) != 0)
+ {
+ LOG_ERR("failed to get Wayland registry");
+ goto out;
+ }
+
+ wl_display_roundtrip(display);
+
+ if (m->manager_wl_name == 0) {
+ LOG_ERR(
+ "compositor does not implement the foreign-toplevel-manager interface");
+ goto out;
+ }
+
+ m->manager = wl_registry_bind(
+ registry, m->manager_wl_name,
+ &zwlr_foreign_toplevel_manager_v1_interface,
+ required_manager_interface_version);
+
+ zwlr_foreign_toplevel_manager_v1_add_listener(
+ m->manager, &manager_listener, mod);
+
+ while (true) {
+ wl_display_flush(display);
+
+ struct pollfd fds[] = {
+ {.fd = mod->abort_fd, .events = POLLIN},
+ {.fd = wl_display_get_fd(display), .events = POLLIN},
+ };
+
+ int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+
+ LOG_ERRNO("failed to poll");
+ break;
+ }
+
+ if (fds[0].revents & (POLLIN | POLLHUP)) {
+ ret = 0;
+ break;
+ }
+
+ if (fds[1].revents & POLLHUP) {
+ LOG_ERR("disconnected from the Wayland compositor");
+ break;
+ }
+
+ assert(fds[1].revents & POLLIN);
+ wl_display_dispatch(display);
+ }
+
+out:
+ tll_foreach(m->toplevels, it) {
+ toplevel_free(&it->item);
+ tll_remove(m->toplevels, it);
+ }
+
+ tll_foreach(m->outputs, it) {
+ output_free(&it->item);
+ tll_remove(m->outputs, it);
+ }
+
+ if (m->xdg_output_manager != NULL)
+ zxdg_output_manager_v1_destroy(m->xdg_output_manager);
+ if (m->manager != NULL)
+ zwlr_foreign_toplevel_manager_v1_destroy(m->manager);
+ if (registry != NULL)
+ wl_registry_destroy(registry);
+ if (display != NULL)
+ wl_display_disconnect(display);
+ return ret;
+}
+
+static struct module *
+ftop_new(struct particle *label, bool all_monitors)
+{
+ struct private *m = calloc(1, sizeof(*m));
+ m->template = label;
+ m->all_monitors = all_monitors;
+
+ struct module *mod = module_common_new();
+ mod->private = m;
+ mod->run = &run;
+ mod->destroy = &destroy;
+ mod->content = &content;
+ mod->description = &description;
+ return mod;
+}
+
+static struct module *
+from_conf(const struct yml_node *node, struct conf_inherit inherited)
+{
+ const struct yml_node *c = yml_get_value(node, "content");
+ const struct yml_node *all_monitors = yml_get_value(node, "all-monitors");
+
+ return ftop_new(
+ conf_to_particle(c, inherited),
+ all_monitors != NULL ? yml_value_as_bool(all_monitors) : false);
+}
+
+static bool
+verify_conf(keychain_t *chain, const struct yml_node *node)
+{
+ static const struct attr_info attrs[] = {
+ {"all-monitors", false, &conf_verify_bool},
+ MODULE_COMMON_ATTRS,
+ };
+
+ return conf_verify_dict(chain, node, attrs);
+}
+
+const struct module_iface module_foreign_toplevel_iface = {
+ .verify_conf = &verify_conf,
+ .from_conf = &from_conf,
+};
+
+#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
+extern const struct module_iface iface __attribute__((weak, alias("module_foreign_toplevel_iface")));
+#endif
diff --git a/modules/meson.build b/modules/meson.build
index 5b480d7..dbaa7a7 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -52,6 +52,28 @@ if backend_wayland
mod_data += {
'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], [dynlist]],
}
+
+ ftop_proto_headers = []
+ ftop_proto_src = []
+
+ foreach prot : ['../external/wlr-foreign-toplevel-management-unstable-v1.xml']
+
+ ftop_proto_headers += custom_target(
+ prot.underscorify() + '-client-header',
+ output: '@BASENAME@.h',
+ input: prot,
+ command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@'])
+
+ ftop_proto_src += custom_target(
+ prot.underscorify() + '-private-code',
+ output: '@BASENAME@.c',
+ input: prot,
+ command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@'])
+ endforeach
+
+ mod_data += {
+ 'foreign-toplevel': [[wl_proto_src + wl_proto_headers + ftop_proto_headers + ftop_proto_src], [dynlist]],
+ }
endif
foreach mod, data : mod_data
diff --git a/plugin.c b/plugin.c
index 05a2c1a..5e0e4bc 100644
--- a/plugin.c
+++ b/plugin.c
@@ -36,6 +36,7 @@ EXTERN_MODULE(alsa);
EXTERN_MODULE(backlight);
EXTERN_MODULE(battery);
EXTERN_MODULE(clock);
+EXTERN_MODULE(foreign_toplevel);
EXTERN_MODULE(i3);
EXTERN_MODULE(label);
EXTERN_MODULE(mpd);
@@ -111,6 +112,9 @@ init(void)
REGISTER_CORE_MODULE(backlight, backlight);
REGISTER_CORE_MODULE(battery, battery);
REGISTER_CORE_MODULE(clock, clock);
+#if defined(HAVE_PLUGIN_foreign_toplevel)
+ REGISTER_CORE_MODULE(foreign-toplevel, foreign_toplevel);
+#endif
REGISTER_CORE_MODULE(i3, i3);
REGISTER_CORE_MODULE(label, label);
REGISTER_CORE_MODULE(mpd, mpd);