rofi/source/modes/wayland-window.c
lbonn ccea31234a [WaylandWindow] Remove a scary log
Happens when destroy() is called without previous init()
2024-03-01 18:26:23 +01:00

635 lines
20 KiB
C

/*
* rofi
*
* MIT/X11 License
* Copyright © 2013-2022 Qball Cow <qball@gmpclient.org>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
/** The log domain of this dialog. */
#define G_LOG_DOMAIN "Modes.Window"
#include "config.h"
#ifdef WINDOW_MODE
#include <stdint.h>
#include <glib.h>
#include <wayland-client.h>
#include "display.h"
#include "helper.h"
#include "modes/wayland-window.h"
#include "rofi.h"
#include "settings.h"
#include "wayland-internal.h"
#include "widgets/textbox.h"
#include "mode-private.h"
#include "rofi-icon-fetcher.h"
#include "wlr-foreign-toplevel-management-unstable-v1-protocol.h"
#define WLR_FOREIGN_TOPLEVEL_VERSION 3
enum WaylandWindowMatchingFields {
WW_MATCH_FIELD_TITLE = 1 << 0,
WW_MATCH_FIELD_APP_ID = 1 << 1,
WW_MATCH_FIELD_ALL = ~0,
};
typedef struct _WaylandWindowModePrivateData {
wayland_stuff *wayland;
struct wl_registry *registry;
struct zwlr_foreign_toplevel_manager_v1 *manager;
GList *toplevels; /* List of ForeignToplevelHandle */
/* initial rendering complete, updates allowed */
gboolean visible;
int match_fields; /* bitmask of WaylandWindowMatchingFields */
glong title_len;
glong app_id_len;
GRegex *window_regex;
} WaylandWindowModePrivateData;
enum ForeignToplevelState {
TOPLEVEL_STATE_MAXIMIZED = 1
<< ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED,
TOPLEVEL_STATE_MINIMIZED = 1
<< ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED,
TOPLEVEL_STATE_ACTIVATED = 1
<< ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED,
TOPLEVEL_STATE_FULLSCREEN =
1 << ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN,
TOPLEVEL_STATE_CLOSED = 1 << 4
};
typedef struct {
struct zwlr_foreign_toplevel_handle_v1 *handle;
WaylandWindowModePrivateData *view;
gchar *app_id;
glong app_id_len;
gchar *title;
glong title_len;
int state;
unsigned int cached_icon_uid;
unsigned int cached_icon_size;
guint cached_icon_scale;
} ForeignToplevelHandle;
static void foreign_toplevel_handle_free(ForeignToplevelHandle *self) {
if (self->handle) {
zwlr_foreign_toplevel_handle_v1_destroy(self->handle);
self->handle = NULL;
}
g_free(self->title);
g_free(self->app_id);
g_free(self);
}
static void toplevels_list_update_max_len(gpointer data, gpointer user_data) {
WaylandWindowModePrivateData *pd = (WaylandWindowModePrivateData *)user_data;
ForeignToplevelHandle *entry = (ForeignToplevelHandle *)data;
pd->title_len = MAX(entry->title_len, pd->title_len);
pd->app_id_len = MAX(entry->app_id_len, pd->app_id_len);
}
/* Update column alignment and schedule reload */
static void wayland_window_update_toplevel(ForeignToplevelHandle *toplevel) {
WaylandWindowModePrivateData *pd = toplevel->view;
if (!pd->visible) {
/* initial fetch, just add the current item */
toplevels_list_update_max_len(toplevel, pd);
} else {
/* async update, recalculate from scratch */
pd->title_len = 0;
pd->app_id_len = 0;
g_list_foreach(pd->toplevels, toplevels_list_update_max_len, pd);
rofi_view_reload();
}
}
/* requests */
static void foreign_toplevel_handle_activate(ForeignToplevelHandle *self,
struct wl_seat *seat) {
zwlr_foreign_toplevel_handle_v1_activate(self->handle, seat);
}
static void foreign_toplevel_handle_close(ForeignToplevelHandle *self) {
zwlr_foreign_toplevel_handle_v1_close(self->handle);
}
/* events */
static void foreign_toplevel_handle_title(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
const char *title) {
ForeignToplevelHandle *self = (ForeignToplevelHandle *)data;
if (self->title) {
g_free(self->title);
}
self->title = g_strdup(title);
self->title_len = g_utf8_strlen(self->title, -1);
}
static void foreign_toplevel_handle_app_id(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
const char *app_id) {
ForeignToplevelHandle *self = (ForeignToplevelHandle *)data;
if (self->app_id) {
g_free(self->app_id);
}
self->app_id = g_strdup(app_id);
self->app_id_len = g_utf8_strlen(self->app_id, -1);
}
static void foreign_toplevel_handle_output_enter(
G_GNUC_UNUSED void *data,
G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
G_GNUC_UNUSED struct wl_output *output) {
/* ignore */
}
static void foreign_toplevel_handle_output_leave(
G_GNUC_UNUSED void *data,
G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
G_GNUC_UNUSED struct wl_output *output) {
/* ignore */
}
static void foreign_toplevel_handle_state(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
struct wl_array *value) {
ForeignToplevelHandle *self = (ForeignToplevelHandle *)data;
uint32_t *elem;
self->state = 0;
wl_array_for_each(elem, value) { self->state |= 1 << *elem; }
}
static void foreign_toplevel_handle_done(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle) {
ForeignToplevelHandle *self = (ForeignToplevelHandle *)data;
g_debug("window %p id=%s title=%s state=%d\n", (void *)self, self->app_id,
self->title, self->state);
wayland_window_update_toplevel(self);
}
static void foreign_toplevel_handle_closed(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle) {
ForeignToplevelHandle *self = (ForeignToplevelHandle *)data;
/* the handle is inert and will receive no further events */
self->state = TOPLEVEL_STATE_CLOSED;
self->view->toplevels = g_list_remove(self->view->toplevels, self);
wayland_window_update_toplevel(self);
foreign_toplevel_handle_free(self);
}
static void foreign_toplevel_handle_parent(
G_GNUC_UNUSED void *data,
G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *handle,
G_GNUC_UNUSED struct zwlr_foreign_toplevel_handle_v1 *parent) {
/* ignore */
}
static struct zwlr_foreign_toplevel_handle_v1_listener
foreign_toplevel_handle_listener = {
.title = &foreign_toplevel_handle_title,
.app_id = &foreign_toplevel_handle_app_id,
.output_enter = &foreign_toplevel_handle_output_enter,
.output_leave = &foreign_toplevel_handle_output_leave,
.state = &foreign_toplevel_handle_state,
.done = &foreign_toplevel_handle_done,
.closed = &foreign_toplevel_handle_closed,
.parent = &foreign_toplevel_handle_parent};
static ForeignToplevelHandle *
foreign_toplevel_handle_new(struct zwlr_foreign_toplevel_handle_v1 *handle,
WaylandWindowModePrivateData *view) {
ForeignToplevelHandle *self =
(ForeignToplevelHandle *)g_malloc0(sizeof(ForeignToplevelHandle));
self->handle = handle;
self->view = view;
zwlr_foreign_toplevel_handle_v1_add_listener(
handle, &foreign_toplevel_handle_listener, self);
return self;
}
static void foreign_toplevel_manager_toplevel(
void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_manager_v1 *manager,
struct zwlr_foreign_toplevel_handle_v1 *toplevel) {
WaylandWindowModePrivateData *pd = (WaylandWindowModePrivateData *)data;
ForeignToplevelHandle *handle = foreign_toplevel_handle_new(toplevel, pd);
pd->toplevels = g_list_prepend(pd->toplevels, handle);
}
static void foreign_toplevel_manager_finished(
G_GNUC_UNUSED void *data,
struct zwlr_foreign_toplevel_manager_v1 *manager) {
zwlr_foreign_toplevel_manager_v1_destroy(manager);
}
static struct zwlr_foreign_toplevel_manager_v1_listener
foreign_toplevel_manager_listener = {
.toplevel = &foreign_toplevel_manager_toplevel,
.finished = &foreign_toplevel_manager_finished};
static void handle_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface,
uint32_t version) {
WaylandWindowModePrivateData *pd = (WaylandWindowModePrivateData *)data;
if (g_strcmp0(interface, zwlr_foreign_toplevel_manager_v1_interface.name) ==
0) {
pd->manager = (struct zwlr_foreign_toplevel_manager_v1 *)wl_registry_bind(
registry, name, &zwlr_foreign_toplevel_manager_v1_interface,
MIN(version, WLR_FOREIGN_TOPLEVEL_VERSION));
}
}
static void handle_global_remove(G_GNUC_UNUSED void *data,
G_GNUC_UNUSED struct wl_registry *registry,
G_GNUC_UNUSED uint32_t name) {}
static struct wl_registry_listener registry_listener = {
.global = &handle_global, .global_remove = &handle_global_remove};
static int wayland_window_mode_parse_fields(void) {
int result = 0;
char *savept = NULL;
// Make a copy, as strtok will modify it.
char *switcher_str = g_strdup(config.window_match_fields);
const char *const sep = ",#";
for (char *token = strtok_r(switcher_str, sep, &savept); token != NULL;
token = strtok_r(NULL, sep, &savept)) {
if (g_strcmp0(token, "all") == 0) {
result |= WW_MATCH_FIELD_ALL;
} else if (g_strcmp0(token, "title") == 0) {
result |= WW_MATCH_FIELD_TITLE;
} else if (g_strcmp0(token, "class") == 0 ||
g_strcmp0(token, "app-id") == 0) {
result |= WW_MATCH_FIELD_APP_ID;
} else {
g_warning("Unsupported window field name :%s. "
"Wayland window switcher supports only 'title' and 'app-id' "
"('class') fields",
token);
}
}
g_free(switcher_str);
return result;
}
static void get_wayland_window(Mode *sw) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
pd->match_fields = wayland_window_mode_parse_fields();
pd->window_regex = g_regex_new("{[-\\w]+(:-?[0-9]+)?}", 0, 0, NULL);
pd->wayland = wayland;
pd->registry = wl_display_get_registry(wayland->display);
wl_registry_add_listener(pd->registry, &registry_listener, pd);
wl_display_roundtrip(wayland->display);
if (pd->manager == NULL) {
g_warning("Unable to initialize Window mode: Wayland compositor does not "
"support wlr-foreign-toplevel-management protocol");
return;
}
zwlr_foreign_toplevel_manager_v1_add_listener(
pd->manager, &foreign_toplevel_manager_listener, pd);
/* fetch initial set of windows */
wl_display_roundtrip(wayland->display);
pd->visible = TRUE;
}
static void toplevels_list_item_free(gpointer data,
G_GNUC_UNUSED gpointer user_data) {
foreign_toplevel_handle_free((ForeignToplevelHandle *)data);
}
static void wayland_window_private_free(WaylandWindowModePrivateData *pd) {
if (pd->toplevels) {
g_list_foreach(pd->toplevels, toplevels_list_item_free, NULL);
g_list_free(pd->toplevels);
pd->toplevels = NULL;
}
if (pd->registry) {
wl_registry_destroy(pd->registry);
pd->registry = NULL;
}
if (pd->manager) {
zwlr_foreign_toplevel_manager_v1_stop(pd->manager);
pd->manager = NULL;
wl_display_roundtrip(pd->wayland->display);
}
if (pd->window_regex) {
g_regex_unref(pd->window_regex);
}
g_free(pd);
}
static int wayland_window_mode_init(Mode *sw) {
/**
* Called on startup when enabled (in modi list)
*/
if (mode_get_private_data(sw) == NULL) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)g_malloc0(
sizeof(WaylandWindowModePrivateData));
mode_set_private_data(sw, (void *)pd);
get_wayland_window(sw);
}
return TRUE;
}
static unsigned int wayland_window_mode_get_num_entries(const Mode *sw) {
const WaylandWindowModePrivateData *pd =
(const WaylandWindowModePrivateData *)mode_get_private_data(sw);
g_return_val_if_fail(pd != NULL, 0);
return g_list_length(pd->toplevels);
}
static ModeMode wayland_window_mode_result(Mode *sw, int mretv,
G_GNUC_UNUSED char **input,
unsigned int selected_line) {
ModeMode retv = MODE_EXIT;
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
g_return_val_if_fail(pd != NULL, retv);
if (mretv & MENU_NEXT) {
retv = NEXT_DIALOG;
} else if (mretv & MENU_PREVIOUS) {
retv = PREVIOUS_DIALOG;
} else if (mretv & MENU_QUICK_SWITCH) {
retv = (ModeMode)(mretv & MENU_LOWER_MASK);
} else if ((mretv & MENU_OK)) {
rofi_view_hide();
ForeignToplevelHandle *toplevel =
(ForeignToplevelHandle *)g_list_nth_data(pd->toplevels, selected_line);
foreign_toplevel_handle_activate(toplevel, pd->wayland->last_seat->seat);
wl_display_flush(pd->wayland->display);
} else if ((mretv & MENU_ENTRY_DELETE) == MENU_ENTRY_DELETE) {
ForeignToplevelHandle *toplevel =
(ForeignToplevelHandle *)g_list_nth_data(pd->toplevels, selected_line);
foreign_toplevel_handle_close(toplevel);
wl_display_flush(pd->wayland->display);
}
return retv;
}
static void wayland_window_mode_destroy(Mode *sw) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
if (pd == NULL) {
return;
}
wayland_window_private_free(pd);
mode_set_private_data(sw, NULL);
}
static int wayland_window_token_match(const Mode *sw, rofi_int_matcher **tokens,
unsigned int index) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
ForeignToplevelHandle *toplevel =
(ForeignToplevelHandle *)g_list_nth_data(pd->toplevels, index);
g_return_val_if_fail(toplevel != NULL, 0);
int match = TRUE;
if (tokens) {
for (int j = 0; match && tokens[j] != NULL; j++) {
int test = 0;
/* See comment in window.c;
* for each token we want to match at least one field.
*/
rofi_int_matcher *ftokens[2] = {tokens[j], NULL};
if ((pd->match_fields & WW_MATCH_FIELD_TITLE) &&
toplevel->title != NULL && toplevel->title[0] != '\0') {
test = helper_token_match(ftokens, toplevel->title);
}
if (test == tokens[j]->invert &&
(pd->match_fields & WW_MATCH_FIELD_APP_ID) &&
toplevel->app_id != NULL && toplevel->app_id[0] != '\0') {
test = helper_token_match(ftokens, toplevel->app_id);
}
if (test == 0) {
match = 0;
}
}
}
return match;
}
static void helper_eval_add_str(GString *str, const char *input, int len,
int max_len, int nc) {
// g_utf8 does not work with NULL string.
const char *input_nn = input ? input : "";
// Both len and max_len are in characters, not bytes.
int spaces = 0;
if (len > 0) {
if (nc > len) {
int bl = g_utf8_offset_to_pointer(input_nn, len) - input_nn;
char *tmp = g_markup_escape_text(input_nn, bl);
g_string_append(str, tmp);
g_free(tmp);
} else {
spaces = len - nc;
char *tmp = g_markup_escape_text(input_nn, -1);
g_string_append(str, tmp);
g_free(tmp);
}
} else {
char *tmp = g_markup_escape_text(input_nn, -1);
g_string_append(str, tmp);
g_free(tmp);
if (len == 0) {
spaces = MAX(0, max_len - nc);
}
}
while (spaces--) {
g_string_append_c(str, ' ');
}
}
struct arg {
const WaylandWindowModePrivateData *pd;
ForeignToplevelHandle *toplevel;
};
static gboolean helper_eval_cb(const GMatchInfo *info, GString *str,
gpointer data) {
struct arg *d = (struct arg *)data;
gchar *match;
// Get the match
match = g_match_info_fetch(info, 0);
if (match != NULL) {
int l = 0;
if (match[2] == ':') {
l = (int)g_ascii_strtoll(&match[3], NULL, 10);
}
/* Most of the arguments are not supported on wayland */
switch (match[1]) {
case 't': /* title */
helper_eval_add_str(str, d->toplevel->title, l, d->pd->title_len,
d->toplevel->title_len);
break;
case 'a': /* app_id */
case 'c': /* class */
helper_eval_add_str(str, d->toplevel->app_id, l, d->pd->app_id_len,
d->toplevel->app_id_len);
break;
}
g_free(match);
}
return FALSE;
}
static char *_generate_display_string(const WaylandWindowModePrivateData *pd,
ForeignToplevelHandle *toplevel) {
struct arg d = {pd, toplevel};
char *res = g_regex_replace_eval(pd->window_regex, config.window_format, -1,
0, 0, helper_eval_cb, &d, NULL);
return g_strchomp(res);
}
static char *_get_display_value(const Mode *sw, unsigned int selected_line,
int *state, G_GNUC_UNUSED GList **attr_list,
int get_entry) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
g_return_val_if_fail(pd != NULL, NULL);
ForeignToplevelHandle *toplevel =
(ForeignToplevelHandle *)g_list_nth_data(pd->toplevels, selected_line);
if (toplevel == NULL || toplevel->state & TOPLEVEL_STATE_CLOSED) {
return get_entry ? g_strdup("Window has vanished") : NULL;
}
/* This may not work because layer-surface holds focus */
if (toplevel->state & TOPLEVEL_STATE_ACTIVATED) {
*state |= ACTIVE;
}
*state |= MARKUP;
return get_entry ? _generate_display_string(pd, toplevel) : NULL;
}
static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line,
unsigned int height) {
WaylandWindowModePrivateData *pd =
(WaylandWindowModePrivateData *)mode_get_private_data(sw);
const guint scale = display_scale();
g_return_val_if_fail(pd != NULL, NULL);
ForeignToplevelHandle *toplevel =
(ForeignToplevelHandle *)g_list_nth_data(pd->toplevels, selected_line);
/* some apps don't have app_id (WM_CLASS). this is fine */
if (toplevel == NULL || toplevel->app_id == NULL ||
toplevel->app_id[0] == '\0') {
return NULL;
}
if (toplevel->cached_icon_uid > 0 && toplevel->cached_icon_size == height &&
toplevel->cached_icon_scale == scale) {
return rofi_icon_fetcher_get(toplevel->cached_icon_uid);
}
/**
* Lookup icon by lowercase app_id.
* There's no API to request multiple names, so we do the same as XCB window
* mode and search for a lowercase WM_CLASS/app_id.
*/
gchar *app_id_lower = g_utf8_strdown(toplevel->app_id, -1);
toplevel->cached_icon_size = height;
toplevel->cached_icon_scale = scale;
toplevel->cached_icon_uid = rofi_icon_fetcher_query(app_id_lower, height);
g_free(app_id_lower);
return rofi_icon_fetcher_get(toplevel->cached_icon_uid);
}
#include "mode-private.h"
Mode wayland_window_mode = {.name = "window",
.cfg_name_key = "display-window",
._init = wayland_window_mode_init,
._destroy = wayland_window_mode_destroy,
._get_num_entries =
wayland_window_mode_get_num_entries,
._result = wayland_window_mode_result,
._token_match = wayland_window_token_match,
._get_display_value = _get_display_value,
._get_icon = _get_icon,
._get_completion = NULL,
._preprocess_input = NULL,
._get_message = NULL,
.private_data = NULL,
.free = NULL,
.type = MODE_TYPE_SWITCHER};
#endif // WINDOW_MODE