]> git.mdlowis.com Git - proto/labwc.git/commitdiff
labnag: add --keyboard-focus option
authortokyo4j <hrak1529@gmail.com>
Wed, 1 Oct 2025 05:49:23 +0000 (14:49 +0900)
committerJohan Malm <johanmalm@users.noreply.github.com>
Mon, 13 Oct 2025 18:03:43 +0000 (19:03 +0100)
The new `--keyboard-focus [none|on-demand|exclusive]` option (default:
`none`) allows to some keyboard controls in labnag:

  Right-arrow or Tab: move the button selection to the right
  Left-arrow or Shift-Tab: move the button selection to the left
  Enter: press the selected button
  Escape: close labnag

The selected button is highlighted with the inner 1px border. Maybe we can
instead use different colors for the selected button, but I prefer the
inner border for now because it doesn't require us to add new color
options or make them inherit labwc's theme.

clients/labnag.c
clients/meson.build
docs/labnag.1.scd

index 0c279b2af5c0f1a0573b05d7d64af55ceb70f38d..31ac4e9d5f0d8f00228ec12937332f74cc7e1bf3 100644 (file)
@@ -19,6 +19,7 @@
 #ifdef __FreeBSD__
 #include <sys/event.h> /* For signalfd() */
 #endif
+#include <sys/mman.h>
 #include <sys/signalfd.h>
 #include <sys/timerfd.h>
 #include <sys/wait.h>
@@ -26,6 +27,7 @@
 #include <unistd.h>
 #include <wayland-cursor.h>
 #include <wlr/util/log.h>
+#include <xkbcommon/xkbcommon.h>
 #include "action-prompt-codes.h"
 #include "pool-buffer.h"
 #include "cursor-shape-v1-client-protocol.h"
@@ -38,6 +40,7 @@ struct conf {
        char *output;
        uint32_t anchors;
        int32_t layer; /* enum zwlr_layer_shell_v1_layer or -1 if unset */
+       enum zwlr_layer_surface_v1_keyboard_interactivity keyboard_focus;
 
        /* Colors */
        uint32_t button_text;
@@ -69,11 +72,18 @@ struct pointer {
        int y;
 };
 
+struct keyboard {
+       struct wl_keyboard *keyboard;
+       struct xkb_keymap *keymap;
+       struct xkb_state *state;
+};
+
 struct seat {
        struct wl_seat *wl_seat;
        uint32_t wl_name;
        struct nag *nag;
        struct pointer pointer;
+       struct keyboard keyboard;
        struct wl_list link; /* nag.seats */
 };
 
@@ -130,6 +140,7 @@ struct nag {
        struct conf *conf;
        char *message;
        struct wl_list buttons;
+       int selected_button;
        struct pollfd pollfds[NR_FDS];
 
        struct {
@@ -409,7 +420,8 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y)
 }
 
 static uint32_t
-render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x)
+render_button(cairo_t *cairo, struct nag *nag, struct button *button,
+               bool selected, int *x)
 {
        int text_width, text_height;
        get_text_size(cairo, nag->conf->font_description, &text_width,
@@ -439,6 +451,14 @@ render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x)
                        button->width, button->height);
        cairo_fill(cairo);
 
+       if (selected) {
+               cairo_set_source_u32(cairo, nag->conf->button_border);
+               cairo_set_line_width(cairo, 1);
+               cairo_rectangle(cairo, button->x + 1.5, button->y + 1.5,
+                       button->width - 3, button->height - 3);
+               cairo_stroke(cairo);
+       }
+
        cairo_set_source_u32(cairo, nag->conf->button_text);
        cairo_move_to(cairo, button->x + padding, button->y + padding);
        render_text(cairo, nag->conf->font_description, 1, true,
@@ -464,11 +484,13 @@ render_to_cairo(cairo_t *cairo, struct nag *nag)
        int x = nag->width - nag->conf->button_margin_right;
        x -= nag->conf->button_gap_close;
 
+       int idx = 0;
        struct button *button;
        wl_list_for_each(button, &nag->buttons, link) {
-               h = render_button(cairo, nag, button, &x);
+               h = render_button(cairo, nag, button, idx == nag->selected_button, &x);
                max_height = h > max_height ? h : max_height;
                x -= nag->conf->button_gap;
+               idx++;
        }
 
        if (nag->details.visible) {
@@ -555,6 +577,15 @@ seat_destroy(struct seat *seat)
        if (seat->pointer.pointer) {
                wl_pointer_destroy(seat->pointer.pointer);
        }
+       if (seat->keyboard.keyboard) {
+               wl_keyboard_destroy(seat->keyboard.keyboard);
+       }
+       if (seat->keyboard.keymap) {
+               xkb_keymap_unref(seat->keyboard.keymap);
+       }
+       if (seat->keyboard.state) {
+               xkb_state_unref(seat->keyboard.state);
+       }
        wl_seat_destroy(seat->wl_seat);
        wl_list_remove(&seat->link);
        free(seat);
@@ -939,12 +970,170 @@ static const struct wl_pointer_listener pointer_listener = {
        .axis_discrete = wl_pointer_axis_discrete,
 };
 
+static void
+wl_keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
+               uint32_t format, int32_t fd, uint32_t size)
+{
+       struct seat *seat = data;
+
+       if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
+               wlr_log(WLR_ERROR, "unreconizable keymap format: %d", format);
+               close(fd);
+               return;
+       }
+
+       char *map_shm = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+       if (map_shm == MAP_FAILED) {
+               wlr_log_errno(WLR_ERROR, "mmap()");
+               close(fd);
+               return;
+       }
+
+       if (seat->keyboard.keymap) {
+               xkb_keymap_unref(seat->keyboard.keymap);
+               seat->keyboard.keymap = NULL;
+       }
+       if (seat->keyboard.state) {
+               xkb_state_unref(seat->keyboard.state);
+               seat->keyboard.state = NULL;
+       }
+       struct xkb_context *xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+       seat->keyboard.keymap = xkb_keymap_new_from_string(xkb, map_shm,
+               XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
+       if (seat->keyboard.keymap) {
+               seat->keyboard.state = xkb_state_new(seat->keyboard.keymap);
+       } else {
+               wlr_log(WLR_ERROR, "failed to compile keymap");
+       }
+       xkb_context_unref(xkb);
+
+       munmap(map_shm, size);
+       close(fd);
+}
+
+static void
+wl_keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+               struct wl_surface *surface, struct wl_array *keys)
+{
+}
+
+static void
+wl_keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+               struct wl_surface *surface)
+{
+}
+
+static void
+wl_keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial,
+               uint32_t time, uint32_t key, uint32_t state)
+{
+       struct seat *seat = data;
+       struct nag *nag = seat->nag;
+
+       if (!seat->keyboard.keymap || !seat->keyboard.state) {
+               wlr_log(WLR_ERROR, "keymap/state unavailable");
+               return;
+       }
+
+       if (state != WL_KEYBOARD_KEY_STATE_PRESSED) {
+               return;
+       }
+
+       key += 8;
+       const xkb_keysym_t *syms;
+       if (!xkb_keymap_key_get_syms_by_level(seat->keyboard.keymap,
+                       key, 0, 0, &syms)) {
+               wlr_log(WLR_ERROR, "failed to translate key: %d", key);
+               return;
+       }
+       xkb_mod_mask_t mods = xkb_state_serialize_mods(seat->keyboard.state,
+                               XKB_STATE_MODS_EFFECTIVE);
+       xkb_mod_index_t shift_idx = xkb_keymap_mod_get_index(
+               seat->keyboard.keymap, XKB_MOD_NAME_SHIFT);
+       bool shift = shift_idx != XKB_MOD_INVALID && (mods & (1 << shift_idx));
+
+       int nr_buttons = wl_list_length(&nag->buttons);
+
+       switch (syms[0]) {
+       case XKB_KEY_Left:
+       case XKB_KEY_Right:
+       case XKB_KEY_Tab: {
+               if (nr_buttons <= 0) {
+                       break;
+               }
+               int direction;
+               if (syms[0] == XKB_KEY_Left || (syms[0] == XKB_KEY_Tab && shift)) {
+                       direction = 1;
+               } else {
+                       direction = -1;
+               }
+               nag->selected_button += nr_buttons + direction;
+               nag->selected_button %= nr_buttons;
+               render_frame(nag);
+               close_pollfd(&nag->pollfds[FD_TIMER]);
+               break;
+       }
+       case XKB_KEY_Escape:
+               exit_status = LAB_EXIT_CANCELLED;
+               nag->run_display = false;
+               break;
+       case XKB_KEY_Return:
+       case XKB_KEY_KP_Enter: {
+               int idx = 0;
+               struct button *button;
+               wl_list_for_each(button, &nag->buttons, link) {
+                       if (idx == nag->selected_button) {
+                               button_execute(nag, button);
+                               close_pollfd(&nag->pollfds[FD_TIMER]);
+                               exit_status = idx;
+                               break;
+                       }
+                       idx++;
+               }
+               break;
+       }
+       }
+}
+
+static void
+wl_keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard,
+               uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched,
+               uint32_t mods_locked, uint32_t group)
+{
+       struct seat *seat = data;
+
+       if (!seat->keyboard.state) {
+               wlr_log(WLR_ERROR, "xkb state unavailable");
+               return;
+       }
+
+       xkb_state_update_mask(seat->keyboard.state, mods_depressed,
+               mods_latched, mods_locked, 0, 0, group);
+}
+
+static void
+wl_keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
+               int32_t rate, int32_t delay)
+{
+}
+
+static const struct wl_keyboard_listener keyboard_listener = {
+       .keymap = wl_keyboard_keymap,
+       .enter = wl_keyboard_enter,
+       .leave = wl_keyboard_leave,
+       .key = wl_keyboard_key,
+       .modifiers = wl_keyboard_modifiers,
+       .repeat_info = wl_keyboard_repeat_info,
+};
+
 static void
 seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
                enum wl_seat_capability caps)
 {
        struct seat *seat = data;
        bool cap_pointer = caps & WL_SEAT_CAPABILITY_POINTER;
+       bool cap_keyboard = caps & WL_SEAT_CAPABILITY_KEYBOARD;
+
        if (cap_pointer && !seat->pointer.pointer) {
                seat->pointer.pointer = wl_seat_get_pointer(wl_seat);
                wl_pointer_add_listener(seat->pointer.pointer,
@@ -953,6 +1142,15 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
                wl_pointer_destroy(seat->pointer.pointer);
                seat->pointer.pointer = NULL;
        }
+
+       if (cap_keyboard && !seat->keyboard.keyboard) {
+               seat->keyboard.keyboard = wl_seat_get_keyboard(wl_seat);
+               wl_keyboard_add_listener(seat->keyboard.keyboard,
+                               &keyboard_listener, seat);
+       } else if (!cap_keyboard && seat->keyboard.keyboard) {
+               wl_keyboard_destroy(seat->keyboard.keyboard);
+               seat->keyboard.keyboard = NULL;
+       }
 }
 
 static void
@@ -1075,7 +1273,7 @@ handle_global(void *data, struct wl_registry *registry, uint32_t name,
                }
        } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
                nag->layer_shell = wl_registry_bind(
-                               registry, name, &zwlr_layer_shell_v1_interface, 1);
+                               registry, name, &zwlr_layer_shell_v1_interface, 4);
        } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) {
                nag->cursor_shape_manager = wl_registry_bind(
                                registry, name, &wp_cursor_shape_manager_v1_interface, 1);
@@ -1170,6 +1368,8 @@ nag_setup(struct nag *nag)
                        &layer_surface_listener, nag);
        zwlr_layer_surface_v1_set_anchor(nag->layer_surface,
                        nag->conf->anchors);
+       zwlr_layer_surface_v1_set_keyboard_interactivity(nag->layer_surface,
+                       nag->conf->keyboard_focus);
 
        wl_registry_destroy(registry);
 
@@ -1250,6 +1450,7 @@ conf_init(struct conf *conf)
                | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
                | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
        conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
+       conf->keyboard_focus = ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE;
        conf->bar_border_thickness = 2;
        conf->message_padding = 8;
        conf->details_border_thickness = 3;
@@ -1357,6 +1558,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
                {"debug", no_argument, NULL, 'd'},
                {"edge", required_argument, NULL, 'e'},
                {"layer", required_argument, NULL, 'y'},
+               {"keyboard-focus", required_argument, NULL, 'k'},
                {"font", required_argument, NULL, 'f'},
                {"help", no_argument, NULL, 'h'},
                {"detailed-message", no_argument, NULL, 'l'},
@@ -1395,6 +1597,8 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
                "  -e, --edge top|bottom           Set the edge to use.\n"
                "  -y, --layer overlay|top|bottom|background\n"
                "                                  Set the layer to use.\n"
+               "  -k, --keyboard-focus none|exclusive|on-demand|\n"
+               "                                  Set the policy for keyboard focus.\n"
                "  -f, --font <font>               Set the font to use.\n"
                "  -h, --help                      Show help message and quit.\n"
                "  -l, --detailed-message          Read a detailed message from stdin.\n"
@@ -1426,7 +1630,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
 
        optind = 1;
        while (1) {
-               int c = getopt_long(argc, argv, "B:Z:c:de:y:f:hlL:m:o:s:t:vx", opts, NULL);
+               int c = getopt_long(argc, argv, "B:Z:c:de:y:k:f:hlL:m:o:s:t:vx", opts, NULL);
                if (c == -1) {
                        break;
                }
@@ -1480,6 +1684,23 @@ nag_parse_options(int argc, char **argv, struct nag *nag,
                                return LAB_EXIT_FAILURE;
                        }
                        break;
+               case 'k':
+                       if (strcmp(optarg, "none") == 0) {
+                               conf->keyboard_focus =
+                                       ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE;
+                       } else if (strcmp(optarg, "exclusive") == 0) {
+                               conf->keyboard_focus =
+                                       ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE;
+                       } else if (strcmp(optarg, "on-demand") == 0) {
+                               conf->keyboard_focus =
+                                       ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND;
+                       } else {
+                               fprintf(stderr, "Invalid keyboard focus: %s\n"
+                                               "Usage: --keyboard-focus none|exclusive|on-demand\n",
+                                               optarg);
+                               return LAB_EXIT_FAILURE;
+                       }
+                       break;
                case 'f': /* Font */
                        pango_font_description_free(conf->font_description);
                        conf->font_description = pango_font_description_from_string(optarg);
@@ -1622,6 +1843,14 @@ main(int argc, char **argv)
                wl_list_insert(nag.buttons.prev, &nag.details.button_details->link);
        }
 
+       int nr_buttons = wl_list_length(&nag.buttons);
+       if (conf.keyboard_focus && nr_buttons > 0) {
+               /* select the leftmost button */
+               nag.selected_button = nr_buttons - 1;
+       } else {
+               nag.selected_button = -1;
+       }
+
        wlr_log(WLR_DEBUG, "Output: %s", nag.conf->output);
        wlr_log(WLR_DEBUG, "Anchors: %lu", (unsigned long)nag.conf->anchors);
        wlr_log(WLR_DEBUG, "Message: %s", nag.message);
index 54b92db82a7fdb2fe11175c25ff43caf9e198dd3..467bc03578f827852644cefa21d06270dd9eae02 100644 (file)
@@ -49,6 +49,7 @@ executable(
     wlroots,
     server_protos,
     epoll_dep,
+    xkbcommon,
   ],
   include_directories: [labwc_inc],
   install: true,
index c8aa4d094274087dc423ecdcb09088e2f355fdaf..a2a36c10c93944464068c6407409a22075d192ae 100644 (file)
@@ -31,6 +31,9 @@ _labnag_ [options...]
 *-y, --layer* overlay|top|bottom|background
        Set the layer to use.
 
+*-k, --keyboard-focus none|exclusive|on-demand*
+       Set the policy for keyboard focus.
+
 *-f, --font* <font>
        Set the font to use.