From a3971feca8c918f629c45f24b9841c2b403f7ee4 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Fri, 10 Apr 2026 14:52:24 +0200 Subject: [PATCH 01/16] menu: implement menu accelerators Menu accelerators are one-letter mnemonics to quickly select/exec items from the current menu. For each menu item, the accelerator is defined as the first character of the item label, converted to lowercase. A different accelerator can be explicitly defined in menu.xml with the special '_' character before the target letter. - Add a field `accelerator` to the `menuitem` struct - Implement `menu_item_select_by_accelerator()` Example: The accelerator for an item with the label "e_macs" is 'm'. --- include/menu/menu.h | 16 ++++++++++++- src/menu/menu.c | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/include/menu/menu.h b/include/menu/menu.h index 46bbca12..736e32b1 100644 --- a/include/menu/menu.h +++ b/include/menu/menu.h @@ -23,6 +23,7 @@ struct menuitem { char *text; char *icon_name; const char *arrow; + char accelerator; struct menu *parent; struct menu *submenu; bool selectable; @@ -66,6 +67,19 @@ struct menu { /* For keyboard support */ void menu_item_select_next(void); void menu_item_select_previous(void); + +/** + * menu_item_select_by_accelerator - selects the next menu item with + * a matching accelerator, starting after the current selection + * + * @accelerator a shortcut to quickly select/open an item, defined in menu.xml + * with an underscore in the item label before the target letter. + * + * Return: a boolean value that represents whether the newly selected item + * needs to be executed. + */ +bool menu_item_select_by_accelerator(char accelerator); + void menu_submenu_enter(void); void menu_submenu_leave(void); bool menu_call_selected_actions(void); @@ -100,7 +114,7 @@ void menu_open_root(struct menu *menu, int x, int y); void menu_process_cursor_motion(struct wlr_scene_node *node); /** - * menu_close_root- close root menu + * menu_close_root - close root menu * * This function will close server.menu_current and set it to NULL. * Asserts that server.input_mode is set to LAB_INPUT_STATE_MENU. diff --git a/src/menu/menu.c b/src/menu/menu.c index 251f9a30..c1169a2a 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -2,6 +2,7 @@ #define _POSIX_C_SOURCE 200809L #include "menu/menu.h" #include +#include #include #include #include @@ -143,6 +144,16 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho menuitem->text = xstrdup(text); menuitem->arrow = show_arrow ? "›" : NULL; + const char *it = text; + menuitem->accelerator = tolower(*it); + while (*it != '\0') { + if (*it == '_' && *it != '\0') { + menuitem->accelerator = *(++it); + break; + } + it++; + } + #if HAVE_LIBSFDO if (rc.menu_show_icons && !string_null_or_empty(icon_name)) { menuitem->icon_name = xstrdup(icon_name); @@ -1460,6 +1471,50 @@ menu_item_select_previous(void) menu_item_select(/* forward */ false); } +bool +menu_item_select_by_accelerator(char accelerator) +{ + struct menu *menu = get_selection_leaf(); + if (!menu) { + return false; + } + + bool needs_exec = true; + bool matched = false; + + struct menuitem *selection = menu->selection.item; + struct wl_list *start = selection ? &selection->link : &menu->menuitems; + struct wl_list *current = start; + struct menuitem *item = NULL; + struct menuitem *next_selection = NULL; + do { + current = current->next; + item = wl_container_of(current, item, link); + if (!matched && item->accelerator == accelerator) { + /* Menuentry with a matching accelerator found */ + next_selection = item; + matched = true; + } else if (matched && item->accelerator == accelerator) { + /* Another menuentry with such accelerator found, + cycle selection instead of executing */ + needs_exec = false; + break; + } + } while (current != start); + + if (next_selection) { + menu_process_item_selection(next_selection); + if (needs_exec && next_selection->submenu) { + /* Submenu was opened, select the first menuitem + without executing */ + needs_exec = false; + menu_submenu_enter(); + } + } + return needs_exec; +} + + bool menu_call_selected_actions(void) { From 72f7a446a6833673974b92dea850a11215cc1b56 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Fri, 10 Apr 2026 15:08:18 +0200 Subject: [PATCH 02/16] keyboard: handle menu accelerators Expand the default case in `handle_menu_keys()`: if caught a printable character, pass it to `menu_item_select_by_accelerator()`. --- src/input/keyboard.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/input/keyboard.c b/src/input/keyboard.c index 2d4bde6e..8d9266b8 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -419,6 +419,19 @@ handle_change_vt_key(struct keyboard *keyboard, return false; } +static char +keysym_to_char(uint32_t keysym) { + if (keysym >= 0x0020 && keysym <= 0x00FF) { + return (char)keysym; + } + + if (keysym >= XKB_KEY_KP_0 && keysym <= XKB_KEY_KP_9) { + return (char)('0' + (keysym - XKB_KEY_KP_0)); + } + + return '\0'; +} + static void handle_menu_keys(struct keysyms *syms) { @@ -447,7 +460,14 @@ handle_menu_keys(struct keysyms *syms) cursor_update_focus(); break; default: - continue; + char accelerator = keysym_to_char(syms->syms[i]); + if (accelerator == '\0') { + continue; + } + if (menu_item_select_by_accelerator(accelerator)) { + menu_call_selected_actions(); + } + break; } break; } From 7575623db8fe320f030679a266a3918817b196cb Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Fri, 10 Apr 2026 16:39:07 +0200 Subject: [PATCH 03/16] menu: fix codestyle warnings --- src/input/keyboard.c | 3 ++- src/menu/menu.c | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/input/keyboard.c b/src/input/keyboard.c index 8d9266b8..f48c4dce 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -420,7 +420,8 @@ handle_change_vt_key(struct keyboard *keyboard, } static char -keysym_to_char(uint32_t keysym) { +keysym_to_char(uint32_t keysym) +{ if (keysym >= 0x0020 && keysym <= 0x00FF) { return (char)keysym; } diff --git a/src/menu/menu.c b/src/menu/menu.c index c1169a2a..b7534088 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -1495,8 +1495,10 @@ menu_item_select_by_accelerator(char accelerator) next_selection = item; matched = true; } else if (matched && item->accelerator == accelerator) { - /* Another menuentry with such accelerator found, - cycle selection instead of executing */ + /* + * Another menuentry with such accelerator found, + * cycle selection instead of executing + */ needs_exec = false; break; } @@ -1505,8 +1507,10 @@ menu_item_select_by_accelerator(char accelerator) if (next_selection) { menu_process_item_selection(next_selection); if (needs_exec && next_selection->submenu) { - /* Submenu was opened, select the first menuitem - without executing */ + /* + * Submenu was opened, select the first menuitem + * without executing + */ needs_exec = false; menu_submenu_enter(); } @@ -1514,7 +1518,6 @@ menu_item_select_by_accelerator(char accelerator) return needs_exec; } - bool menu_call_selected_actions(void) { From 9108809578c2ad59104cb550452a0176161c5d7e Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Fri, 10 Apr 2026 19:28:38 +0200 Subject: [PATCH 04/16] menu: fix non-matched accelerators triggering exec --- src/menu/menu.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index b7534088..bedf17d2 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -1507,15 +1507,12 @@ menu_item_select_by_accelerator(char accelerator) if (next_selection) { menu_process_item_selection(next_selection); if (needs_exec && next_selection->submenu) { - /* - * Submenu was opened, select the first menuitem - * without executing - */ + /* Since we can't execute a submenu, enter it instead. */ needs_exec = false; menu_submenu_enter(); } } - return needs_exec; + return matched && needs_exec; } bool From ac72c105cc516dc95feab0ace17d9fea99e7fe01 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Fri, 10 Apr 2026 19:29:28 +0200 Subject: [PATCH 05/16] keyboard: fix "label followed by a declaration" error --- src/input/keyboard.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/input/keyboard.c b/src/input/keyboard.c index f48c4dce..0c5bd802 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -460,7 +460,7 @@ handle_menu_keys(struct keysyms *syms) menu_close_root(); cursor_update_focus(); break; - default: + default: { char accelerator = keysym_to_char(syms->syms[i]); if (accelerator == '\0') { continue; @@ -469,7 +469,7 @@ handle_menu_keys(struct keysyms *syms) menu_call_selected_actions(); } break; - } + }} break; } } From 60a3963ba9cdb50d4255c79fe284796b8a1b47d9 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Sat, 11 Apr 2026 02:14:35 +0200 Subject: [PATCH 06/16] menu: fix wrong accelerator for capital letters --- src/menu/menu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index bedf17d2..d5a83b2f 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -148,7 +148,7 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho menuitem->accelerator = tolower(*it); while (*it != '\0') { if (*it == '_' && *it != '\0') { - menuitem->accelerator = *(++it); + menuitem->accelerator = tolower(*(++it)); break; } it++; From 310c15058dcece8d2d554ec1808365f0955297df Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Sat, 11 Apr 2026 02:28:44 +0200 Subject: [PATCH 07/16] menu: simplified conditionals in menu_item_select_by_accelerator() --- src/menu/menu.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index d5a83b2f..8df2127c 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -1479,7 +1479,7 @@ menu_item_select_by_accelerator(char accelerator) return false; } - bool needs_exec = true; + bool needs_exec = false; bool matched = false; struct menuitem *selection = menu->selection.item; @@ -1490,29 +1490,32 @@ menu_item_select_by_accelerator(char accelerator) do { current = current->next; item = wl_container_of(current, item, link); - if (!matched && item->accelerator == accelerator) { - /* Menuentry with a matching accelerator found */ - next_selection = item; - matched = true; - } else if (matched && item->accelerator == accelerator) { - /* - * Another menuentry with such accelerator found, - * cycle selection instead of executing - */ - needs_exec = false; - break; + if (item->accelerator == accelerator) { + if (!matched) { + /* Found first match */ + next_selection = item; + needs_exec = true; + matched = true; + } else { + /* + * Found another match, + * cycle selection instead of executing + */ + needs_exec = false; + break; + } } } while (current != start); if (next_selection) { menu_process_item_selection(next_selection); if (needs_exec && next_selection->submenu) { - /* Since we can't execute a submenu, enter it instead. */ + /* Since we can't execute a submenu, enter it. */ needs_exec = false; menu_submenu_enter(); } } - return matched && needs_exec; + return needs_exec; } bool From eba57831f455923558be96cef876c4b08b0f1d3e Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Sat, 11 Apr 2026 14:02:14 +0200 Subject: [PATCH 08/16] menu: fix possible buffer over-read in item_create() --- src/menu/menu.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index 8df2127c..deec5123 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -147,8 +147,8 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho const char *it = text; menuitem->accelerator = tolower(*it); while (*it != '\0') { - if (*it == '_' && *it != '\0') { - menuitem->accelerator = tolower(*(++it)); + if (*it == '_') { + menuitem->accelerator = tolower(*(it + 1)); break; } it++; From 667707c02971412df8680b1527e9b72550c3e307 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Sat, 11 Apr 2026 16:46:41 +0200 Subject: [PATCH 09/16] menu: prevent memory leak when cycling through menu items --- src/menu/menu.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index deec5123..7522e6bc 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -1475,7 +1475,7 @@ bool menu_item_select_by_accelerator(char accelerator) { struct menu *menu = get_selection_leaf(); - if (!menu) { + if (!menu || wl_list_empty(&menu->menuitems)) { return false; } @@ -1489,6 +1489,10 @@ menu_item_select_by_accelerator(char accelerator) struct menuitem *next_selection = NULL; do { current = current->next; + if (current == &menu->menuitems) { + /* Allow wrap around */ + continue; + } item = wl_container_of(current, item, link); if (item->accelerator == accelerator) { if (!matched) { From cea22422d57cd6c22c8113d4af9121dc1d3c873e Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Sat, 11 Apr 2026 21:21:10 +0200 Subject: [PATCH 10/16] menu: skip whitespace and unicode emojis before defining accelerators Skip any character whose value in the ASCII table is larger than 127 (I think all non-ASCII chars follow this rule). Also skip any whitespace. --- src/menu/menu.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/menu/menu.c b/src/menu/menu.c index 7522e6bc..013ee7af 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -145,6 +145,17 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho menuitem->arrow = show_arrow ? "›" : NULL; const char *it = text; + /* Skip emojis and whitespace */ + while (*it != '\0') { + unsigned char c = (unsigned char)*it; + + if (isspace(c) || c > 127) { + it++; + } else { + break; + } + } + menuitem->accelerator = tolower(*it); while (*it != '\0') { if (*it == '_') { From 1c646af14448fa0d36ce56c0e47a76a752ab8595 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Mon, 13 Apr 2026 23:06:08 +0200 Subject: [PATCH 11/16] menu: allow for Unicode accelerators --- include/menu/menu.h | 4 ++-- src/input/keyboard.c | 22 +++++----------------- src/menu/menu.c | 40 +++++++++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/include/menu/menu.h b/include/menu/menu.h index 736e32b1..e0e39cf5 100644 --- a/include/menu/menu.h +++ b/include/menu/menu.h @@ -23,7 +23,7 @@ struct menuitem { char *text; char *icon_name; const char *arrow; - char accelerator; + uint32_t accelerator; struct menu *parent; struct menu *submenu; bool selectable; @@ -78,7 +78,7 @@ void menu_item_select_previous(void); * Return: a boolean value that represents whether the newly selected item * needs to be executed. */ -bool menu_item_select_by_accelerator(char accelerator); +bool menu_item_select_by_accelerator(uint32_t accelerator); void menu_submenu_enter(void); void menu_submenu_leave(void); diff --git a/src/input/keyboard.c b/src/input/keyboard.c index 0c5bd802..16b51947 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -419,20 +419,6 @@ handle_change_vt_key(struct keyboard *keyboard, return false; } -static char -keysym_to_char(uint32_t keysym) -{ - if (keysym >= 0x0020 && keysym <= 0x00FF) { - return (char)keysym; - } - - if (keysym >= XKB_KEY_KP_0 && keysym <= XKB_KEY_KP_9) { - return (char)('0' + (keysym - XKB_KEY_KP_0)); - } - - return '\0'; -} - static void handle_menu_keys(struct keysyms *syms) { @@ -454,15 +440,17 @@ handle_menu_keys(struct keysyms *syms) break; case XKB_KEY_Return: case XKB_KEY_KP_Enter: - menu_call_selected_actions(); + if (!menu_call_selected_actions()) { + menu_submenu_enter(); + }; break; case XKB_KEY_Escape: menu_close_root(); cursor_update_focus(); break; default: { - char accelerator = keysym_to_char(syms->syms[i]); - if (accelerator == '\0') { + uint32_t accelerator = xkb_keysym_to_utf32(syms->syms[i]); + if (accelerator == 0) { continue; } if (menu_item_select_by_accelerator(accelerator)) { diff --git a/src/menu/menu.c b/src/menu/menu.c index 013ee7af..f56b208f 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -131,7 +133,7 @@ validate(void) } static struct menuitem * -item_create(struct menu *menu, const char *text, const char *icon_name, bool show_arrow) +item_create(struct menu *menu, char *text, const char *icon_name, bool show_arrow) { assert(menu); assert(text); @@ -145,26 +147,34 @@ item_create(struct menu *menu, const char *text, const char *icon_name, bool sho menuitem->arrow = show_arrow ? "›" : NULL; const char *it = text; - /* Skip emojis and whitespace */ - while (*it != '\0') { - unsigned char c = (unsigned char)*it; - - if (isspace(c) || c > 127) { - it++; - } else { - break; - } - } - - menuitem->accelerator = tolower(*it); + uint32_t accelerator = 0; while (*it != '\0') { if (*it == '_') { - menuitem->accelerator = tolower(*(it + 1)); + char32_t codepoint = 0; + mbstate_t state = {0}; + size_t bytes = mbrtoc32(&codepoint, it + 1, MB_CUR_MAX, &state); + if (bytes > 0 && bytes <= 4) { + accelerator = (uint32_t)towlower((wint_t)codepoint); + } + break; } it++; } + /* Fallback to the first character of the label */ + if (accelerator == 0 && text[0] != '\0') { + char32_t codepoint = 0; + mbstate_t state = {0}; + + size_t bytes = mbrtoc32(&codepoint, text, MB_CUR_MAX, &state); + if (bytes > 0 && bytes <= 4) { + accelerator = (uint32_t)towlower((wint_t)codepoint); + } + } + + menuitem->accelerator = accelerator; + #if HAVE_LIBSFDO if (rc.menu_show_icons && !string_null_or_empty(icon_name)) { menuitem->icon_name = xstrdup(icon_name); @@ -1483,7 +1493,7 @@ menu_item_select_previous(void) } bool -menu_item_select_by_accelerator(char accelerator) +menu_item_select_by_accelerator(uint32_t accelerator) { struct menu *menu = get_selection_leaf(); if (!menu || wl_list_empty(&menu->menuitems)) { From dc28d0caf3e87134bc380713e4fa2d0ac1fafaf5 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Tue, 14 Apr 2026 17:21:22 +0200 Subject: [PATCH 12/16] menu: underline explicitly defined accelerators --- include/common/font.h | 4 +++- src/common/font.c | 4 ++-- src/menu/menu.c | 26 ++++++++++++++++++++++---- src/scaled-buffer/scaled-font-buffer.c | 2 +- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/include/common/font.h b/include/common/font.h index a7a5ba55..22be2ae8 100644 --- a/include/common/font.h +++ b/include/common/font.h @@ -4,6 +4,7 @@ #include #include +#include struct lab_data_buffer; @@ -43,10 +44,11 @@ void font_get_buffer_size(int max_width, const char *text, struct font *font, * @font: font description * @color: foreground color in rgba format * @bg_pattern: background pattern + * @use_markup: flag to render pango markup */ void font_buffer_create(struct lab_data_buffer **buffer, int max_width, int height, const char *text, struct font *font, const float *color, - cairo_pattern_t *bg_pattern, double scale); + cairo_pattern_t *bg_pattern, double scale, bool use_markup); /** * font_finish - free some font related resources diff --git a/src/common/font.c b/src/common/font.c index b307729c..2285b9bf 100644 --- a/src/common/font.c +++ b/src/common/font.c @@ -79,7 +79,7 @@ font_get_buffer_size(int max_width, const char *text, struct font *font, void font_buffer_create(struct lab_data_buffer **buffer, int max_width, int height, const char *text, struct font *font, const float *color, - cairo_pattern_t *bg_pattern, double scale) + cairo_pattern_t *bg_pattern, double scale, bool use_markup) { if (string_null_or_empty(text)) { return; @@ -123,7 +123,7 @@ font_buffer_create(struct lab_data_buffer **buffer, int max_width, PangoLayout *layout = pango_cairo_create_layout(cairo); pango_context_set_round_glyph_positions(pango_layout_get_context(layout), false); pango_layout_set_width(layout, width * PANGO_SCALE); - pango_layout_set_text(layout, text, -1); + use_markup ? pango_layout_set_markup(layout, text, -1) : pango_layout_set_text(layout, text, -1); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); if (!opaque_bg) { diff --git a/src/menu/menu.c b/src/menu/menu.c index f56b208f..372f662f 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -143,18 +143,31 @@ item_create(struct menu *menu, char *text, const char *icon_name, bool show_arro menuitem->parent = menu; menuitem->selectable = true; menuitem->type = LAB_MENU_ITEM; - menuitem->text = xstrdup(text); menuitem->arrow = show_arrow ? "›" : NULL; - const char *it = text; uint32_t accelerator = 0; + char *new_text = NULL; + char *it = text; while (*it != '\0') { if (*it == '_') { char32_t codepoint = 0; mbstate_t state = {0}; - size_t bytes = mbrtoc32(&codepoint, it + 1, MB_CUR_MAX, &state); + size_t bytes = mbrtoc32(&codepoint, it + 1, + MB_CUR_MAX, &state); if (bytes > 0 && bytes <= 4) { - accelerator = (uint32_t)towlower((wint_t)codepoint); + accelerator = + (uint32_t)towlower((wint_t)codepoint); + } + + if (*(it + 1) != '\0') { + int underscore_index = it - text; + new_text = malloc(strlen(text) + 8); + if (!new_text) { + break; + } + memcpy(new_text, text, underscore_index); + sprintf(new_text + underscore_index, "%c%s", + *(it + 1), it + 2); } break; @@ -174,6 +187,11 @@ item_create(struct menu *menu, char *text, const char *icon_name, bool show_arro } menuitem->accelerator = accelerator; + if (new_text) { + menuitem->text = xstrdup(new_text); + } else { + menuitem->text = xstrdup(text); + } #if HAVE_LIBSFDO if (rc.menu_show_icons && !string_null_or_empty(icon_name)) { diff --git a/src/scaled-buffer/scaled-font-buffer.c b/src/scaled-buffer/scaled-font-buffer.c index bb93fc67..49c9d840 100644 --- a/src/scaled-buffer/scaled-font-buffer.c +++ b/src/scaled-buffer/scaled-font-buffer.c @@ -26,7 +26,7 @@ _create_buffer(struct scaled_buffer *scaled_buffer, double scale) /* Buffer gets free'd automatically along the backing wlr_buffer */ font_buffer_create(&buffer, self->max_width, self->height, self->text, - &self->font, self->color, bg_pattern, scale); + &self->font, self->color, bg_pattern, scale, true); if (!buffer) { wlr_log(WLR_ERROR, "font_buffer_create() failed"); From 58fa0897499a5351e83a2a0a8ed85bc870c1e922 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Tue, 14 Apr 2026 17:35:46 +0200 Subject: [PATCH 13/16] font: fix stylecheck errors --- src/common/font.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/font.c b/src/common/font.c index 2285b9bf..5162d284 100644 --- a/src/common/font.c +++ b/src/common/font.c @@ -123,7 +123,13 @@ font_buffer_create(struct lab_data_buffer **buffer, int max_width, PangoLayout *layout = pango_cairo_create_layout(cairo); pango_context_set_round_glyph_positions(pango_layout_get_context(layout), false); pango_layout_set_width(layout, width * PANGO_SCALE); - use_markup ? pango_layout_set_markup(layout, text, -1) : pango_layout_set_text(layout, text, -1); + + if (use_markup) { + pango_layout_set_markup(layout, text, -1); + } else { + pango_layout_set_text(layout, text, -1); + } + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); if (!opaque_bg) { From c6681876b5efd750acd732b42ff0eff790a2594f Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Wed, 15 Apr 2026 00:15:12 +0200 Subject: [PATCH 14/16] tree-wide: use markup only when an accelerator is defined with _ --- include/menu/menu.h | 1 + include/scaled-buffer/scaled-font-buffer.h | 3 ++- src/cycle/osd-classic.c | 6 +++--- src/cycle/osd-thumbnail.c | 2 +- src/menu/menu.c | 24 +++++++++------------- src/scaled-buffer/scaled-font-buffer.c | 6 ++++-- src/ssd/resize-indicator.c | 2 +- src/ssd/ssd-titlebar.c | 2 +- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/include/menu/menu.h b/include/menu/menu.h index e0e39cf5..3d546efa 100644 --- a/include/menu/menu.h +++ b/include/menu/menu.h @@ -27,6 +27,7 @@ struct menuitem { struct menu *parent; struct menu *submenu; bool selectable; + bool use_markup; enum menuitem_type type; int native_width; struct wlr_scene_tree *tree; diff --git a/include/scaled-buffer/scaled-font-buffer.h b/include/scaled-buffer/scaled-font-buffer.h index a5e95087..a76fe03f 100644 --- a/include/scaled-buffer/scaled-font-buffer.h +++ b/include/scaled-buffer/scaled-font-buffer.h @@ -15,6 +15,7 @@ struct scaled_font_buffer { /* Private */ char *text; + bool use_markup; int max_width; float color[4]; float bg_color[4]; @@ -71,6 +72,6 @@ scaled_font_buffer_create_for_titlebar(struct wlr_scene_tree *parent, */ void scaled_font_buffer_update(struct scaled_font_buffer *self, const char *text, int max_width, struct font *font, const float *color, - const float *bg_color); + const float *bg_color, bool use_markup); #endif /* LABWC_SCALED_FONT_BUFFER_H */ diff --git a/src/cycle/osd-classic.c b/src/cycle/osd-classic.c index 7e5abfb6..685b58b8 100644 --- a/src/cycle/osd-classic.c +++ b/src/cycle/osd-classic.c @@ -58,8 +58,8 @@ create_fields_scene(struct view *view, struct scaled_font_buffer *font_buffer = scaled_font_buffer_create(parent); scaled_font_buffer_update(font_buffer, - buf.data, field_width, - &rc.font_osd, text_color, bg_color); + buf.data, field_width, &rc.font_osd, + text_color, bg_color, false); node = &font_buffer->scene_buffer->node; height = font_height(&rc.font_osd); } @@ -143,7 +143,7 @@ cycle_osd_classic_init(struct cycle_osd_output *osd_output) wlr_scene_node_set_position(&font_buffer->scene_buffer->node, x, y + (switcher_theme->item_height - font_height(&font)) / 2); scaled_font_buffer_update(font_buffer, workspace_name, 0, - &font, text_color, bg_color); + &font, text_color, bg_color, false); y += switcher_theme->item_height; } diff --git a/src/cycle/osd-thumbnail.c b/src/cycle/osd-thumbnail.c index 623708a0..eba0249e 100644 --- a/src/cycle/osd-thumbnail.c +++ b/src/cycle/osd-thumbnail.c @@ -112,7 +112,7 @@ create_label(struct wlr_scene_tree *parent, struct view *view, scaled_font_buffer_create(parent); scaled_font_buffer_update(buffer, buf.data, switcher_theme->item_width - 2 * switcher_theme->item_padding, - &rc.font_osd, text_color, bg_color); + &rc.font_osd, text_color, bg_color, false); buf_reset(&buf); wlr_scene_node_set_position(&buffer->scene_buffer->node, (switcher_theme->item_width - buffer->width) / 2, y); diff --git a/src/menu/menu.c b/src/menu/menu.c index 372f662f..58a2b96a 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -155,22 +155,16 @@ item_create(struct menu *menu, char *text, const char *icon_name, bool show_arro size_t bytes = mbrtoc32(&codepoint, it + 1, MB_CUR_MAX, &state); if (bytes > 0 && bytes <= 4) { - accelerator = - (uint32_t)towlower((wint_t)codepoint); + accelerator = (uint32_t)towlower((wint_t)codepoint); } if (*(it + 1) != '\0') { int underscore_index = it - text; - new_text = malloc(strlen(text) + 8); - if (!new_text) { - break; - } - memcpy(new_text, text, underscore_index); - sprintf(new_text + underscore_index, "%c%s", - *(it + 1), it + 2); + new_text = strdup_printf("%.*s%.*s%s", + underscore_index, text, (int)bytes, + it + 1, it + 1 + bytes); + break; } - - break; } it++; } @@ -189,8 +183,10 @@ item_create(struct menu *menu, char *text, const char *icon_name, bool show_arro menuitem->accelerator = accelerator; if (new_text) { menuitem->text = xstrdup(new_text); + menuitem->use_markup = true; } else { menuitem->text = xstrdup(text); + menuitem->use_markup = false; } #if HAVE_LIBSFDO @@ -263,7 +259,7 @@ item_create_scene_for_state(struct menuitem *item, float *text_color, struct scaled_font_buffer *label_buffer = scaled_font_buffer_create(tree); assert(label_buffer); scaled_font_buffer_update(label_buffer, item->text, label_max_width, - &rc.font_menuitem, text_color, bg_color); + &rc.font_menuitem, text_color, bg_color, item->use_markup); /* Vertically center and left-align label */ int x = theme->menu_items_padding_x + icon_width; int y = (theme->menu_item_height - label_buffer->height) / 2; @@ -277,7 +273,7 @@ item_create_scene_for_state(struct menuitem *item, float *text_color, struct scaled_font_buffer *arrow_buffer = scaled_font_buffer_create(tree); assert(arrow_buffer); scaled_font_buffer_update(arrow_buffer, item->arrow, -1, - &rc.font_menuitem, text_color, bg_color); + &rc.font_menuitem, text_color, bg_color, false); /* Vertically center and right-align arrow */ x += label_max_width + theme->menu_items_padding_x; y = (theme->menu_item_height - label_buffer->height) / 2; @@ -416,7 +412,7 @@ title_create_scene(struct menuitem *menuitem, int *item_y) scaled_font_buffer_create(menuitem->normal_tree); assert(title_font_buffer); scaled_font_buffer_update(title_font_buffer, menuitem->text, - text_width, &rc.font_menuheader, text_color, bg_color); + text_width, &rc.font_menuheader, text_color, bg_color, false); int title_x = 0; switch (theme->menu_title_text_justify) { diff --git a/src/scaled-buffer/scaled-font-buffer.c b/src/scaled-buffer/scaled-font-buffer.c index 49c9d840..229d97d9 100644 --- a/src/scaled-buffer/scaled-font-buffer.c +++ b/src/scaled-buffer/scaled-font-buffer.c @@ -26,7 +26,7 @@ _create_buffer(struct scaled_buffer *scaled_buffer, double scale) /* Buffer gets free'd automatically along the backing wlr_buffer */ font_buffer_create(&buffer, self->max_width, self->height, self->text, - &self->font, self->color, bg_pattern, scale, true); + &self->font, self->color, bg_pattern, scale, self->use_markup); if (!buffer) { wlr_log(WLR_ERROR, "font_buffer_create() failed"); @@ -56,6 +56,7 @@ _equal(struct scaled_buffer *scaled_buffer_a, struct scaled_font_buffer *b = scaled_buffer_b->data; return str_equal(a->text, b->text) + && a->use_markup == b->use_markup && a->max_width == b->max_width && str_equal(a->font.name, b->font.name) && a->font.size == b->font.size @@ -107,7 +108,7 @@ scaled_font_buffer_create_for_titlebar(struct wlr_scene_tree *parent, void scaled_font_buffer_update(struct scaled_font_buffer *self, const char *text, int max_width, struct font *font, const float *color, - const float *bg_color) + const float *bg_color, bool use_markup) { assert(self); assert(text); @@ -120,6 +121,7 @@ scaled_font_buffer_update(struct scaled_font_buffer *self, const char *text, /* Update internal state */ self->text = xstrdup(text); + self->use_markup = use_markup; self->max_width = max_width; if (font->name) { self->font.name = xstrdup(font->name); diff --git a/src/ssd/resize-indicator.c b/src/ssd/resize-indicator.c index fa1166a3..45df44e1 100644 --- a/src/ssd/resize-indicator.c +++ b/src/ssd/resize-indicator.c @@ -202,7 +202,7 @@ resize_indicator_update(struct view *view) wlr_scene_node_set_position(&indicator->tree->node, x, y); scaled_font_buffer_update(indicator->text, text, width, &rc.font_osd, - rc.theme->osd_label_text_color, rc.theme->osd_bg_color); + rc.theme->osd_label_text_color, rc.theme->osd_bg_color, false); } void diff --git a/src/ssd/ssd-titlebar.c b/src/ssd/ssd-titlebar.c index d1a08810..816221db 100644 --- a/src/ssd/ssd-titlebar.c +++ b/src/ssd/ssd-titlebar.c @@ -473,7 +473,7 @@ ssd_update_title(struct ssd *ssd) const float bg_color[4] = {0, 0, 0, 0}; /* ignored */ scaled_font_buffer_update(subtree->title, view->title, title_bg_width, font, - text_color, bg_color); + text_color, bg_color, false); /* And finally update the cache */ dstate->width = subtree->title->width; From f1155e00203a4cb7050c123e0fdd738c4d9b3a1a Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Wed, 15 Apr 2026 00:56:25 +0200 Subject: [PATCH 15/16] key-state: pass missing `use_markup` argument ..to `update_key_indicator_callback()` --- src/input/key-state.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/input/key-state.c b/src/input/key-state.c index 6d089891..ea8897a6 100644 --- a/src/input/key-state.c +++ b/src/input/key-state.c @@ -116,7 +116,7 @@ update_key_indicator_callback(void *data) buf_add_fmt(&buf, "%s (%d), ", keyname, pressed.values[i]); } scaled_font_buffer_update(indicator_state.sfb_pressed, buf.data, - -1, &rc.font_osd, black, white); + -1, &rc.font_osd, black, white, false); buf_clear(&buf); buf_add(&buf, "bound="); @@ -126,7 +126,7 @@ update_key_indicator_callback(void *data) buf_add_fmt(&buf, "%s (%d), ", keyname, bound.values[i]); } scaled_font_buffer_update(indicator_state.sfb_bound, buf.data, - -1, &rc.font_osd, black, white); + -1, &rc.font_osd, black, white, false); buf_clear(&buf); buf_add(&buf, "pressed_sent="); @@ -136,7 +136,7 @@ update_key_indicator_callback(void *data) buf_add_fmt(&buf, "%s (%d), ", keyname, pressed_sent.values[i]); } scaled_font_buffer_update(indicator_state.sfb_pressed_sent, buf.data, -1, - &rc.font_osd, black, white); + &rc.font_osd, black, white, false); buf_clear(&buf); buf_add(&buf, "modifiers="); @@ -148,7 +148,7 @@ update_key_indicator_callback(void *data) } buf_add_fmt(&buf, "(%d)", all_modifiers); scaled_font_buffer_update(indicator_state.sfb_modifiers, buf.data, -1, - &rc.font_osd, black, white); + &rc.font_osd, black, white, false); buf_reset(&buf); } From d49a2d3ad1c7af70eae3e5bc5e410352b5ceaec7 Mon Sep 17 00:00:00 2001 From: Alex Chernika Date: Wed, 15 Apr 2026 13:09:27 +0200 Subject: [PATCH 16/16] menu: use `new_text` directly instead of copying Since otherwise it causes a memory leak: `new_text` used to be allocated, but never free'd. --- src/menu/menu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/menu.c b/src/menu/menu.c index 58a2b96a..310cd736 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -182,7 +182,7 @@ item_create(struct menu *menu, char *text, const char *icon_name, bool show_arro menuitem->accelerator = accelerator; if (new_text) { - menuitem->text = xstrdup(new_text); + menuitem->text = new_text; menuitem->use_markup = true; } else { menuitem->text = xstrdup(text);