// SPDX-License-Identifier: GPL-2.0-only #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #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, LAB_ACTION_ARG_INT, }; 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; }; struct action_arg_int { struct action_arg base; int 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_TOGGLE_ALWAYS_ON_BOTTOM, ACTION_TYPE_FOCUS, ACTION_TYPE_ICONIFY, ACTION_TYPE_MOVE, ACTION_TYPE_RAISE, ACTION_TYPE_LOWER, ACTION_TYPE_RESIZE, ACTION_TYPE_RESIZE_RELATIVE, ACTION_TYPE_MOVETO, ACTION_TYPE_MOVE_RELATIVE, ACTION_TYPE_SEND_TO_DESKTOP, ACTION_TYPE_GO_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", "ToggleAlwaysOnBottom", "Focus", "Iconify", "Move", "Raise", "Lower", "Resize", "ResizeRelative", "MoveTo", "MoveRelative", "SendToDesktop", "GoToDesktop", "SnapToRegion", "ToggleKeybinds", "FocusOutput", NULL }; void action_arg_add_str(struct action *action, const char *key, const char *value) { assert(action); assert(key); assert(value && "Tried to add NULL action string argument"); struct action_arg_str *arg = znew(*arg); arg->base.type = LAB_ACTION_ARG_STR; 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, const char *key, bool value) { assert(action); assert(key); struct action_arg_bool *arg = znew(*arg); arg->base.type = LAB_ACTION_ARG_BOOL; arg->base.key = xstrdup(key); arg->value = value; wl_list_append(&action->args, &arg->base.link); } static void action_arg_add_int(struct action *action, const char *key, int value) { assert(action); assert(key); struct action_arg_int *arg = znew(*arg); arg->base.type = LAB_ACTION_ARG_INT; 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, const char *nodename, const char *content) { assert(action); char *argument = xstrdup(nodename); string_truncate_at_pattern(argument, ".action"); switch (action->type) { case ACTION_TYPE_EXECUTE: /* * with an child is * deprecated, but we support it anyway for backward * compatibility with old openbox-menu generators */ if (!strcmp(argument, "command") || !strcmp(argument, "execute")) { 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")) { enum view_edge edge = view_edge_parse(content); if ((edge == VIEW_EDGE_CENTER && action->type != ACTION_TYPE_SNAP_TO_EDGE) || edge == VIEW_EDGE_INVALID) { wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s' (%s)", action_names[action->type], argument, content); } else { action_arg_add_int(action, argument, edge); } 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_RESIZE_RELATIVE: if (!strcmp(argument, "left") || !strcmp(argument, "right") || !strcmp(argument, "top") || !strcmp(argument, "bottom")) { action_arg_add_int(action, argument, atoi(content)); goto cleanup; } break; case ACTION_TYPE_MOVETO: case ACTION_TYPE_MOVE_RELATIVE: if (!strcmp(argument, "x") || !strcmp(argument, "y")) { action_arg_add_int(action, argument, atoi(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; } if (!strcmp(argument, "wrap")) { action_arg_add_bool(action, argument, parse_bool(content, true)); 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 bool arg_value_exists(struct action *action, const char *key, enum action_arg_type type) { assert(action); assert(key); struct action_arg *arg; wl_list_for_each(arg, &action->args, link) { if (!strcasecmp(key, arg->key) && arg->type == type) { return true; } } return false; } static const char * get_arg_value_str(struct action *action, const char *key, const char *default_value) { assert(action); assert(key); struct action_arg *arg; wl_list_for_each(arg, &action->args, link) { 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(action); assert(key); struct action_arg *arg; wl_list_for_each(arg, &action->args, link) { if (!strcasecmp(key, arg->key)) { assert(arg->type == LAB_ACTION_ARG_BOOL); return ((struct action_arg_bool *)arg)->value; } } return default_value; } static int get_arg_value_int(struct action *action, const char *key, int default_value) { assert(action); assert(key); struct action_arg *arg; wl_list_for_each(arg, &action->args, link) { if (!strcasecmp(key, arg->key)) { assert(arg->type == LAB_ACTION_ARG_INT); return ((struct action_arg_int *)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; } /* Checks for *required* arguments */ bool action_is_valid(struct action *action) { const char *arg_name = NULL; enum action_arg_type arg_type = LAB_ACTION_ARG_STR; switch (action->type) { case ACTION_TYPE_EXECUTE: arg_name = "command"; break; case ACTION_TYPE_MOVE_TO_EDGE: case ACTION_TYPE_SNAP_TO_EDGE: arg_name = "direction"; arg_type = LAB_ACTION_ARG_INT; break; case ACTION_TYPE_SHOW_MENU: arg_name = "menu"; break; case ACTION_TYPE_GO_TO_DESKTOP: case ACTION_TYPE_SEND_TO_DESKTOP: arg_name = "to"; break; case ACTION_TYPE_SNAP_TO_REGION: arg_name = "region"; break; case ACTION_TYPE_FOCUS_OUTPUT: arg_name = "output"; break; default: /* No arguments required */ return true; } if (arg_value_exists(action, arg_name, arg_type)) { return true; } wlr_log(WLR_ERROR, "Missing required argument for %s: %s", action_names[action->type], arg_name); return false; } void action_free(struct action *action) { /* Free args */ struct action_arg *arg, *arg_tmp; 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) { struct action_arg_str *str_arg = (struct action_arg_str *)arg; zfree(str_arg->value); } zfree(arg); } zfree(action); } void action_list_free(struct wl_list *action_list) { struct action *action, *action_tmp; wl_list_for_each_safe(action, action_tmp, action_list, link) { wl_list_remove(&action->link); action_free(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: { 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 (view) { /* Config parsing makes sure that direction is a valid direction */ enum view_edge edge = get_arg_value_int(action, "direction", 0); view_move_to_edge(view, edge); } break; case ACTION_TYPE_SNAP_TO_EDGE: if (view) { /* Config parsing makes sure that direction is a valid direction */ enum view_edge edge = get_arg_value_int(action, "direction", 0); view_snap_to_edge(view, edge, /*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: show_menu(server, view, action_str_from_arg(arg)); 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_TOGGLE_ALWAYS_ON_BOTTOM: if (view) { view_toggle_always_on_bottom(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_RESIZE_RELATIVE: if (view) { int left = get_arg_value_int(action, "left", 0); int right = get_arg_value_int(action, "right", 0); int top = get_arg_value_int(action, "top", 0); int bottom = get_arg_value_int(action, "bottom", 0); view_resize_relative(view, left, right, top, bottom); } break; case ACTION_TYPE_MOVETO: if (view) { int x = get_arg_value_int(action, "x", 0); int y = get_arg_value_int(action, "y", 0); view_move(view, x, y); } break; case ACTION_TYPE_MOVE_RELATIVE: if (view) { int x = get_arg_value_int(action, "x", 0); int y = get_arg_value_int(action, "y", 0); view_move_relative(view, x, y); } break; case ACTION_TYPE_SEND_TO_DESKTOP: if (!view) { break; } /* Falls through to GoToDesktop */ case ACTION_TYPE_GO_TO_DESKTOP: { bool follow = true; bool wrap = get_arg_value_bool(action, "wrap", true); const char *to = get_arg_value_str(action, "to", NULL); /* * `to` is always != NULL here because otherwise we would have * removed the action during the initial parsing step as it is * a required argument for both SendToDesktop and GoToDesktop. */ struct workspace *target = workspaces_find( server->workspace_current, to, wrap); if (!target) { break; } if (action->type == ACTION_TYPE_SEND_TO_DESKTOP) { view_move_to_workspace(view, target); follow = get_arg_value_bool(action, "follow", true); } if (follow) { workspaces_switch_to(target); } } break; case ACTION_TYPE_SNAP_TO_REGION: 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: if (view) { view_toggle_keybinds(view); } break; case ACTION_TYPE_FOCUS_OUTPUT: { 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); } } }