menu: support pipemenu with the toplevel <menu> element

For example:

    <?xml version="1.0"?>
    <openbox_menu>
      <menu id="root-menu" label="" execute="obmenu-generator"/>
    </openbox_menu>

Fixes: #2238

Co-Authored-By: @Consolatis
This commit is contained in:
Johan Malm 2024-10-14 19:46:03 +01:00 committed by Consolatis
parent a93eb84335
commit 956b271f9b
3 changed files with 90 additions and 15 deletions

View file

@ -112,6 +112,16 @@ ID attributes are unique. Duplicates are ignored.
When writing pipe menu scripts, make sure to escape XML special characters such When writing pipe menu scripts, make sure to escape XML special characters such
as "&" ("&amp;"), "<" ("&lt;"), and ">" ("&gt;"). as "&" ("&amp;"), "<" ("&lt;"), and ">" ("&gt;").
A pipemenu can also be used to define the toplevel *<menu>* element. In this
case the entire menu.xml file would be reduced to something like this (replacing
obmenu-generator with the menu generator of your choice):
```
<?xml version="1.0"?>
<openbox_menu>
<menu id="root-menu" label="" execute="obmenu-generator"/>
</openbox_menu>
```
# LOCALISATION # LOCALISATION

View file

@ -42,6 +42,7 @@ struct menu {
char *id; char *id;
char *label; char *label;
struct menu *parent; struct menu *parent;
struct menu_pipe_context *pipe_ctx;
struct { struct {
int width; int width;

View file

@ -48,6 +48,7 @@ static struct menuitem *selected_item;
struct menu_pipe_context { struct menu_pipe_context {
struct server *server; struct server *server;
struct menuitem *item; struct menuitem *item;
struct menu *top_level_menu;
struct buf buf; struct buf buf;
struct wl_event_source *event_read; struct wl_event_source *event_read;
struct wl_event_source *event_timeout; struct wl_event_source *event_timeout;
@ -606,6 +607,36 @@ is_toplevel_static_menu_definition(xmlNode *n, char *id)
return id && nr_parents(n) == 2; return id && nr_parents(n) == 2;
} }
static bool parse_buf(struct server *server, struct buf *buf);
static int handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx);
static int handle_pipemenu_timeout(void *_ctx);
static void
parse_root_pipemenu(struct menu *top_level_menu, const char *execute)
{
int pipe_fd = 0;
pid_t pid = spawn_piped(execute, &pipe_fd);
if (pid <= 0) {
wlr_log(WLR_ERROR, "Failed to spawn pipe menu process %s", execute);
return;
}
struct menu_pipe_context *ctx = znew(*ctx);
ctx->server = top_level_menu->server;
ctx->top_level_menu = top_level_menu;
ctx->pid = pid;
ctx->pipe_fd = pipe_fd;
ctx->buf = BUF_INIT;
top_level_menu->pipe_ctx = ctx;
ctx->event_read = wl_event_loop_add_fd(ctx->server->wl_event_loop,
pipe_fd, WL_EVENT_READABLE, handle_pipemenu_readable, ctx);
ctx->event_timeout = wl_event_loop_add_timer(ctx->server->wl_event_loop,
handle_pipemenu_timeout, ctx);
wl_event_source_timer_update(ctx->event_timeout, PIPEMENU_TIMEOUT_IN_MS);
}
/* /*
* <menu> elements have three different roles: * <menu> elements have three different roles:
* * Definition of (sub)menu - has ID, LABEL and CONTENT * * Definition of (sub)menu - has ID, LABEL and CONTENT
@ -623,25 +654,22 @@ handle_menu_element(xmlNode *n, struct server *server)
wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute); wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute);
if (!current_menu) { if (!current_menu) {
/* /*
* We currently do not support pipemenus without a * Handle pipemenu as the root-menu such this:
* parent <item> such as the one the example below:
* *
* <?xml version="1.0" encoding="UTF-8"?> * <?xml version="1.0" encoding="UTF-8"?>
* <openbox_menu> * <openbox_menu>
* <menu id="root-menu" label="foo" execute="bar"/> * <menu id="root-menu" label="foo" execute="bar"/>
* </openbox_menu> * </openbox_menu>
*
* TODO: Consider supporting this
*/ */
wlr_log(WLR_ERROR, struct menu *menu = menu_create(server, id, label);
"pipemenu '%s:%s:%s' has no parent <menu>", parse_root_pipemenu(menu, execute);
id, label, execute); } else {
goto error; current_item = item_create(current_menu, label,
/* arrow */ true);
current_item_action = NULL;
current_item->execute = xstrdup(execute);
current_item->id = xstrdup(id);
} }
current_item = item_create(current_menu, label, /* arrow */ true);
current_item_action = NULL;
current_item->execute = xstrdup(execute);
current_item->id = xstrdup(id);
} else if ((label && id) || is_toplevel_static_menu_definition(n, id)) { } else if ((label && id) || is_toplevel_static_menu_definition(n, id)) {
/* /*
* (label && id) refers to <menu id="" label=""> which is an * (label && id) refers to <menu id="" label=""> which is an
@ -1167,6 +1195,10 @@ menu_free(struct menu *menu)
item_destroy(item); item_destroy(item);
} }
if (menu->pipe_ctx) {
menu->pipe_ctx->top_level_menu = NULL;
}
/* /*
* Destroying the root node will destroy everything, * Destroying the root node will destroy everything,
* including node descriptors and scaled_font_buffers. * including node descriptors and scaled_font_buffers.
@ -1344,6 +1376,34 @@ menu_open_root(struct menu *menu, int x, int y)
static void static void
create_pipe_menu(struct menu_pipe_context *ctx) create_pipe_menu(struct menu_pipe_context *ctx)
{ {
if (ctx->top_level_menu) {
/*
* We execute the scripts for the toplevel pipemenus at startup
* or Reconfigure, but they can be opened before they finish
* execution, usually with their content empty. Make sure they
* are closed and emptied.
*/
if (ctx->server->menu_current == ctx->top_level_menu) {
menu_close_root(ctx->server);
}
struct menuitem *item, *tmp;
wl_list_for_each_safe(item, tmp, &ctx->top_level_menu->menuitems, link) {
item_destroy(item);
}
menu_level++;
current_menu = ctx->top_level_menu;
if (!parse_buf(ctx->server, &ctx->buf)) {
wlr_log(WLR_ERROR, "Failed to parse piped top level menu %s",
ctx->top_level_menu->id);
}
menu_level--;
post_processing(ctx->server);
validate(ctx->server);
menu_update_scene(current_menu);
return;
}
assert(ctx->item); assert(ctx->item);
struct menu *pipe_parent = ctx->item->parent; struct menu *pipe_parent = ctx->item->parent;
@ -1409,6 +1469,9 @@ pipemenu_ctx_destroy(struct menu_pipe_context *ctx)
if (ctx->item) { if (ctx->item) {
ctx->item->pipe_ctx = NULL; ctx->item->pipe_ctx = NULL;
} }
if (ctx->top_level_menu) {
ctx->top_level_menu->pipe_ctx = NULL;
}
free(ctx); free(ctx);
waiting_for_pipe_menu = false; waiting_for_pipe_menu = false;
} }
@ -1432,7 +1495,7 @@ handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx)
char data[8193]; char data[8193];
ssize_t size; ssize_t size;
if (!ctx->item) { if (!ctx->item && !ctx->top_level_menu) {
/* parent menu item got destroyed in the meantime */ /* parent menu item got destroyed in the meantime */
wlr_log(WLR_INFO, "[pipemenu %ld] parent menu item destroyed", wlr_log(WLR_INFO, "[pipemenu %ld] parent menu item destroyed",
(long)ctx->pid); (long)ctx->pid);
@ -1447,14 +1510,15 @@ handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx)
if (size == -1) { if (size == -1) {
wlr_log_errno(WLR_ERROR, "[pipemenu %ld] failed to read data (%s)", wlr_log_errno(WLR_ERROR, "[pipemenu %ld] failed to read data (%s)",
(long)ctx->pid, ctx->item->execute); (long)ctx->pid, ctx->item ? ctx->item->execute : "n/a");
goto clean_up; goto clean_up;
} }
/* Limit pipemenu buffer to 1 MiB for safety */ /* Limit pipemenu buffer to 1 MiB for safety */
if (ctx->buf.len + size > PIPEMENU_MAX_BUF_SIZE) { if (ctx->buf.len + size > PIPEMENU_MAX_BUF_SIZE) {
wlr_log(WLR_ERROR, "[pipemenu %ld] too big (> %d bytes); killing %s", wlr_log(WLR_ERROR, "[pipemenu %ld] too big (> %d bytes); killing %s",
(long)ctx->pid, PIPEMENU_MAX_BUF_SIZE, ctx->item->execute); (long)ctx->pid, PIPEMENU_MAX_BUF_SIZE,
ctx->item ? ctx->item->execute : "n/a");
kill(ctx->pid, SIGTERM); kill(ctx->pid, SIGTERM);
goto clean_up; goto clean_up;
} }