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..dafc60f
--- /dev/null
+++ b/modules/foreign-toplevel.c
@@ -0,0 +1,395 @@
+#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"
+
+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;
+};
+
+struct private {
+ struct particle *template;
+ struct zwlr_foreign_toplevel_manager_v1 *manager;
+ tll(struct toplevel) toplevels;
+};
+
+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);
+}
+
+static void
+destroy(struct module *mod)
+{
+ struct private *m = mod->private;
+ m->template->destroy(m->template);
+
+ tll_foreach(m->toplevels, it) {
+ toplevel_free(&it->item);
+ tll_remove(m->toplevels, it);
+ }
+
+ 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);
+
+ size_t toplevel_count = tll_length(m->toplevels);
+ struct exposable *toplevels[toplevel_count];
+
+ size_t idx = 0;
+ tll_foreach(m->toplevels, it) {
+ 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[idx++] = m->template->instantiate(m->template, &tags);
+ tag_set_destroy(&tags);
+ }
+
+ mtx_unlock(&mod->lock);
+ return dynlist_exposable_new(toplevels, toplevel_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
+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 *output)
+{
+}
+
+static void
+output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *output)
+{
+}
+
+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 private *m = top->mod->private;
+
+ tll_foreach(m->toplevels, it) {
+ if (it->item.handle == handle) {
+ toplevel_free(top);
+ tll_remove(m->toplevels, it);
+ break;
+ }
+ }
+}
+
+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,
+ };
+
+ tll_push_back(m->toplevels, toplevel);
+
+ zwlr_foreign_toplevel_handle_v1_add_listener(
+ handle, &toplevel_listener, &tll_back(m->toplevels));
+}
+
+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
+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) {
+ const uint32_t required = 2;
+ if (!verify_iface_version(interface, version, required))
+ return;
+
+ m->manager = wl_registry_bind(
+ registry, name,
+ &zwlr_foreign_toplevel_manager_v1_interface, required);
+
+ zwlr_foreign_toplevel_manager_v1_add_listener(
+ m->manager, &manager_listener, mod);
+ }
+}
+
+static void
+handle_global_remove(void *data, struct wl_registry *registry, uint32_t name)
+{
+}
+
+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);
+
+ 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);
+ }
+
+ 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)
+{
+ struct private *m = calloc(1, sizeof(*m));
+ m->template = label;
+
+ 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");
+ return ftop_new(conf_to_particle(c, inherited));
+}
+
+static bool
+verify_conf(keychain_t *chain, const struct yml_node *node)
+{
+ static const struct attr_info attrs[] = {
+ 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..23111ae 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': [[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);