labwc/src/action.c
Consolatis de4a7ce8ee src/action.c: validate action type when assigning arguments
Before this patch it was possible to assign an argument with an
existing name to an action that didn't support the given argument.
An example of this is using `direction` for `GoToDesktop`.

This patch now only creates action arguments that are actually
defined for the given action type and logs an error for
unsupported arguments.

The commit also makes sure to always supply the argument name.
This will reduce required checks in other parts of the codebase
in future commits.

Partly fixes: #894
2023-05-15 21:35:14 +02:00

628 lines
15 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <wlr/util/log.h>
#include "action.h"
#include "common/list.h"
#include "common/mem.h"
#include "common/parse-bool.h"
#include "common/spawn.h"
#include "common/string-helpers.h"
#include "debug.h"
#include "labwc.h"
#include "menu/menu.h"
#include "regions.h"
#include "ssd.h"
#include "view.h"
#include "workspaces.h"
enum action_arg_type {
LAB_ACTION_ARG_STR = 0,
LAB_ACTION_ARG_BOOL,
};
struct action_arg {
struct wl_list link; /* struct action.args */
char *key; /* May be NULL if there is just one arg */
enum action_arg_type type;
};
struct action_arg_str {
struct action_arg base;
char *value;
};
struct action_arg_bool {
struct action_arg base;
bool value;
};
enum action_type {
ACTION_TYPE_INVALID = 0,
ACTION_TYPE_NONE,
ACTION_TYPE_CLOSE,
ACTION_TYPE_KILL,
ACTION_TYPE_DEBUG,
ACTION_TYPE_EXECUTE,
ACTION_TYPE_EXIT,
ACTION_TYPE_MOVE_TO_EDGE,
ACTION_TYPE_SNAP_TO_EDGE,
ACTION_TYPE_NEXT_WINDOW,
ACTION_TYPE_PREVIOUS_WINDOW,
ACTION_TYPE_RECONFIGURE,
ACTION_TYPE_SHOW_MENU,
ACTION_TYPE_TOGGLE_MAXIMIZE,
ACTION_TYPE_MAXIMIZE,
ACTION_TYPE_TOGGLE_FULLSCREEN,
ACTION_TYPE_TOGGLE_DECORATIONS,
ACTION_TYPE_TOGGLE_ALWAYS_ON_TOP,
ACTION_TYPE_FOCUS,
ACTION_TYPE_ICONIFY,
ACTION_TYPE_MOVE,
ACTION_TYPE_RAISE,
ACTION_TYPE_LOWER,
ACTION_TYPE_RESIZE,
ACTION_TYPE_GO_TO_DESKTOP,
ACTION_TYPE_SEND_TO_DESKTOP,
ACTION_TYPE_SNAP_TO_REGION,
ACTION_TYPE_TOGGLE_KEYBINDS,
ACTION_TYPE_FOCUS_OUTPUT,
};
const char *action_names[] = {
"INVALID",
"None",
"Close",
"Kill",
"Debug",
"Execute",
"Exit",
"MoveToEdge",
"SnapToEdge",
"NextWindow",
"PreviousWindow",
"Reconfigure",
"ShowMenu",
"ToggleMaximize",
"Maximize",
"ToggleFullscreen",
"ToggleDecorations",
"ToggleAlwaysOnTop",
"Focus",
"Iconify",
"Move",
"Raise",
"Lower",
"Resize",
"GoToDesktop",
"SendToDesktop",
"SnapToRegion",
"ToggleKeybinds",
"FocusOutput",
NULL
};
void
action_arg_add_str(struct action *action, char *key, const char *value)
{
assert(value && "Tried to add NULL action string argument");
struct action_arg_str *arg = znew(*arg);
arg->base.type = LAB_ACTION_ARG_STR;
if (key) {
arg->base.key = xstrdup(key);
}
arg->value = xstrdup(value);
wl_list_append(&action->args, &arg->base.link);
}
static void
action_arg_add_bool(struct action *action, char *key, bool value)
{
struct action_arg_bool *arg = znew(*arg);
arg->base.type = LAB_ACTION_ARG_BOOL;
if (key) {
arg->base.key = xstrdup(key);
}
arg->value = value;
wl_list_append(&action->args, &arg->base.link);
}
void
action_arg_from_xml_node(struct action *action, char *nodename, char *content)
{
assert(action);
char *argument = xstrdup(nodename);
string_truncate_at_pattern(argument, ".action");
switch (action->type) {
case ACTION_TYPE_EXECUTE:
if (!strcmp(argument, "command")) {
action_arg_add_str(action, argument, content);
goto cleanup;
} else if (!strcmp(argument, "execute")) {
/*
* <action name="Execute"><execute>foo</execute></action>
* is deprecated, but we support it anyway for backward
* compatibility with old openbox-menu generators
*/
action_arg_add_str(action, "command", content);
goto cleanup;
}
break;
case ACTION_TYPE_MOVE_TO_EDGE:
case ACTION_TYPE_SNAP_TO_EDGE:
if (!strcmp(argument, "direction")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
case ACTION_TYPE_SHOW_MENU:
if (!strcmp(argument, "menu")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
case ACTION_TYPE_SEND_TO_DESKTOP:
if (!strcmp(argument, "follow")) {
action_arg_add_bool(action, argument, parse_bool(content, true));
goto cleanup;
}
/* Falls through to GoToDesktop */
case ACTION_TYPE_GO_TO_DESKTOP:
if (!strcmp(argument, "to")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
case ACTION_TYPE_SNAP_TO_REGION:
if (!strcmp(argument, "region")) {
action_arg_add_str(action, argument, content);
goto cleanup;
};
break;
case ACTION_TYPE_FOCUS_OUTPUT:
if (!strcmp(argument, "output")) {
action_arg_add_str(action, argument, content);
goto cleanup;
}
break;
}
wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s'",
action_names[action->type], argument);
cleanup:
free(argument);
}
static const char *
action_str_from_arg(struct action_arg *arg)
{
assert(arg->type == LAB_ACTION_ARG_STR);
return ((struct action_arg_str *)arg)->value;
}
static const char *
get_arg_value_str(struct action *action, const char *key, const char *default_value)
{
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!arg->key) {
continue;
}
if (!strcasecmp(key, arg->key)) {
return action_str_from_arg(arg);
}
}
return default_value;
}
static bool
get_arg_value_bool(struct action *action, const char *key, bool default_value)
{
assert(key);
struct action_arg *arg;
wl_list_for_each(arg, &action->args, link) {
if (!arg->key) {
continue;
}
if (!strcasecmp(key, arg->key)) {
assert(arg->type == LAB_ACTION_ARG_BOOL);
return ((struct action_arg_bool *)arg)->value;
}
}
return default_value;
}
static struct action_arg *
action_get_first_arg(struct action *action)
{
struct action_arg *arg;
struct wl_list *item = action->args.next;
if (item == &action->args) {
return NULL;
}
return wl_container_of(item, arg, link);
}
static enum action_type
action_type_from_str(const char *action_name)
{
for (size_t i = 1; action_names[i]; i++) {
if (!strcasecmp(action_name, action_names[i])) {
return i;
}
}
wlr_log(WLR_ERROR, "Invalid action: %s", action_name);
return ACTION_TYPE_INVALID;
}
struct action *
action_create(const char *action_name)
{
if (!action_name) {
wlr_log(WLR_ERROR, "action name not specified");
return NULL;
}
enum action_type action_type = action_type_from_str(action_name);
if (action_type == ACTION_TYPE_NONE) {
return NULL;
}
struct action *action = znew(*action);
action->type = action_type;
wl_list_init(&action->args);
return action;
}
bool
actions_contain_toggle_keybinds(struct wl_list *action_list)
{
struct action *action;
wl_list_for_each(action, action_list, link) {
if (action->type == ACTION_TYPE_TOGGLE_KEYBINDS) {
return true;
}
}
return false;
}
void action_list_free(struct wl_list *action_list)
{
struct action_arg *arg, *arg_tmp;
struct action *action, *action_tmp;
/* Free actions */
wl_list_for_each_safe(action, action_tmp, action_list, link) {
wl_list_remove(&action->link);
/* Free args */
wl_list_for_each_safe(arg, arg_tmp, &action->args, link) {
wl_list_remove(&arg->link);
zfree(arg->key);
if (arg->type == LAB_ACTION_ARG_STR) {
free((void *)action_str_from_arg(arg));
}
zfree(arg);
}
zfree(action);
}
}
static void
show_menu(struct server *server, struct view *view, const char *menu_name)
{
if (server->input_mode != LAB_INPUT_STATE_PASSTHROUGH
&& server->input_mode != LAB_INPUT_STATE_MENU) {
/* Prevent opening a menu while resizing / moving a view */
return;
}
bool force_menu_top_left = false;
struct menu *menu = menu_get_by_id(menu_name);
if (!menu) {
return;
}
if (!strcasecmp(menu_name, "client-menu")) {
if (!view) {
return;
}
enum ssd_part_type type = ssd_at(view->ssd, server->scene,
server->seat.cursor->x, server->seat.cursor->y);
if (type == LAB_SSD_BUTTON_WINDOW_MENU) {
force_menu_top_left = true;
} else if (ssd_part_contains(LAB_SSD_PART_TITLEBAR, type)) {
force_menu_top_left = false;
} else {
force_menu_top_left = true;
}
}
int x, y;
if (force_menu_top_left) {
x = view->current.x;
y = view->current.y;
} else {
x = server->seat.cursor->x;
y = server->seat.cursor->y;
}
/* Replaced by next show_menu() or cleaned on view_destroy() */
menu->triggered_by_view = view;
menu_open(menu, x, y);
}
static struct view *
view_for_action(struct view *activator, struct server *server,
struct action *action, uint32_t *resize_edges)
{
/* View is explicitly specified for mousebinds */
if (activator) {
return activator;
}
/* Select view based on action type for keybinds */
switch (action->type) {
case ACTION_TYPE_FOCUS:
case ACTION_TYPE_MOVE:
case ACTION_TYPE_RESIZE: {
struct cursor_context ctx = get_cursor_context(server);
if (action->type == ACTION_TYPE_RESIZE) {
/* Select resize edges for the keybind case */
*resize_edges = cursor_get_resize_edges(
server->seat.cursor, &ctx);
}
return ctx.view;
}
default:
return desktop_focused_view(server);
}
}
void
actions_run(struct view *activator, struct server *server,
struct wl_list *actions, uint32_t resize_edges)
{
if (!actions) {
wlr_log(WLR_ERROR, "empty actions");
return;
}
struct view *view;
struct action *action;
struct action_arg *arg;
wl_list_for_each(action, actions, link) {
/* Get arg now so we don't have to repeat every time we only need one */
arg = action_get_first_arg(action);
if (arg && arg->type == LAB_ACTION_ARG_STR) {
wlr_log(WLR_DEBUG, "Handling action %u: %s %s",
action->type, action_names[action->type],
action_str_from_arg(arg));
} else {
wlr_log(WLR_DEBUG, "Handling action %u: %s",
action->type, action_names[action->type]);
}
/*
* Refetch view because it may have been changed due to the
* previous action
*/
view = view_for_action(activator, server, action,
&resize_edges);
switch (action->type) {
case ACTION_TYPE_CLOSE:
if (view) {
view_close(view);
}
break;
case ACTION_TYPE_KILL:
if (view && view->surface) {
/* Send SIGTERM to the process associated with the surface */
pid_t pid = -1;
struct wl_client *client = view->surface->resource->client;
wl_client_get_credentials(client, &pid, NULL, NULL);
if (pid != -1) {
kill(pid, SIGTERM);
}
}
break;
case ACTION_TYPE_DEBUG:
debug_dump_scene(server);
break;
case ACTION_TYPE_EXECUTE:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for Execute");
break;
}
struct buf cmd;
buf_init(&cmd);
buf_add(&cmd, action_str_from_arg(arg));
buf_expand_shell_variables(&cmd);
spawn_async_no_shell(cmd.buf);
free(cmd.buf);
break;
case ACTION_TYPE_EXIT:
wl_display_terminate(server->wl_display);
break;
case ACTION_TYPE_MOVE_TO_EDGE:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for MoveToEdge");
break;
}
if (view) {
view_move_to_edge(view, action_str_from_arg(arg));
}
break;
case ACTION_TYPE_SNAP_TO_EDGE:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for SnapToEdge");
break;
}
if (view) {
view_snap_to_edge(view, action_str_from_arg(arg),
/*store_natural_geometry*/ true);
}
break;
case ACTION_TYPE_NEXT_WINDOW:
server->osd_state.cycle_view = desktop_cycle_view(server,
server->osd_state.cycle_view, LAB_CYCLE_DIR_FORWARD);
osd_update(server);
break;
case ACTION_TYPE_PREVIOUS_WINDOW:
server->osd_state.cycle_view = desktop_cycle_view(server,
server->osd_state.cycle_view, LAB_CYCLE_DIR_BACKWARD);
osd_update(server);
break;
case ACTION_TYPE_RECONFIGURE:
kill(getpid(), SIGHUP);
break;
case ACTION_TYPE_SHOW_MENU:
if (arg) {
show_menu(server, view, action_str_from_arg(arg));
} else {
wlr_log(WLR_ERROR, "Missing argument for ShowMenu");
}
break;
case ACTION_TYPE_TOGGLE_MAXIMIZE:
if (view) {
view_toggle_maximize(view);
}
break;
case ACTION_TYPE_MAXIMIZE:
if (view) {
view_maximize(view, true, /*store_natural_geometry*/ true);
}
break;
case ACTION_TYPE_TOGGLE_FULLSCREEN:
if (view) {
view_toggle_fullscreen(view);
}
break;
case ACTION_TYPE_TOGGLE_DECORATIONS:
if (view) {
view_toggle_decorations(view);
}
break;
case ACTION_TYPE_TOGGLE_ALWAYS_ON_TOP:
if (view) {
view_toggle_always_on_top(view);
}
break;
case ACTION_TYPE_FOCUS:
if (view) {
desktop_focus_and_activate_view(&server->seat, view);
}
break;
case ACTION_TYPE_ICONIFY:
if (view) {
view_minimize(view, true);
}
break;
case ACTION_TYPE_MOVE:
if (view) {
interactive_begin(view, LAB_INPUT_STATE_MOVE, 0);
}
break;
case ACTION_TYPE_RAISE:
if (view) {
view_move_to_front(view);
}
break;
case ACTION_TYPE_LOWER:
if (view) {
view_move_to_back(view);
}
break;
case ACTION_TYPE_RESIZE:
if (view) {
interactive_begin(view, LAB_INPUT_STATE_RESIZE,
resize_edges);
}
break;
case ACTION_TYPE_GO_TO_DESKTOP:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for GoToDesktop");
break;
}
struct workspace *target;
const char *target_name = action_str_from_arg(arg);
target = workspaces_find(server->workspace_current, target_name);
if (target) {
workspaces_switch_to(target);
}
break;
case ACTION_TYPE_SEND_TO_DESKTOP:
if (view) {
const char *to = get_arg_value_str(action, "to", NULL);
if (!to) {
wlr_log(WLR_ERROR,
"Missing 'to' argument for SendToDesktop");
break;
}
bool follow = get_arg_value_bool(action, "follow", true);
struct workspace *target = workspaces_find(view->workspace, to);
if (target) {
view_move_to_workspace(view, target);
if (follow) {
workspaces_switch_to(target);
}
}
}
break;
case ACTION_TYPE_SNAP_TO_REGION:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for SnapToRegion");
break;
}
if (!view) {
break;
}
struct output *output = view->output;
if (!output) {
break;
}
const char *region_name = action_str_from_arg(arg);
struct region *region = regions_from_name(region_name, output);
if (region) {
view_snap_to_region(view, region,
/*store_natural_geometry*/ true);
} else {
wlr_log(WLR_ERROR, "Invalid SnapToRegion id: '%s'", region_name);
}
break;
case ACTION_TYPE_TOGGLE_KEYBINDS:
server->seat.inhibit_keybinds = !server->seat.inhibit_keybinds;
wlr_log(WLR_DEBUG, "%s keybinds",
server->seat.inhibit_keybinds ? "Disabled" : "Enabled");
break;
case ACTION_TYPE_FOCUS_OUTPUT:
if (!arg) {
wlr_log(WLR_ERROR, "Missing argument for FocusOutput");
break;
}
const char *output_name = action_str_from_arg(arg);
desktop_focus_output(output_from_name(server, output_name));
break;
case ACTION_TYPE_INVALID:
wlr_log(WLR_ERROR, "Not executing unknown action");
break;
default:
/*
* If we get here it must be a BUG caused most likely by
* action_names and action_type being out of sync or by
* adding a new action without installing a handler here.
*/
wlr_log(WLR_ERROR,
"Not executing invalid action (%u)"
" This is a BUG. Please report.", action->type);
}
}
}