diff --git a/docs/userguide b/docs/userguide index 59833ed5..05e10da4 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2215,7 +2215,10 @@ https://developer.gnome.org/pango/stable/PangoMarkupFormat.html[Pango markup] and the following placeholders which will be replaced: +%title+:: - The X11 window title (_NET_WM_NAME or WM_NAME as fallback). + For normal windows, this is the X11 window title (_NET_WM_NAME or WM_NAME + as fallback). When used on containers without a window (e.g., a split + container inside a tabbed/stacked layout), this will be the tree + representation of the container (e.g., "H[xterm xterm]"). +%class+:: The X11 window class (second part of WM_CLASS). This corresponds to the +class+ criterion, see <>. diff --git a/i3bar/include/util.h b/i3bar/include/util.h index ba08cf76..dfbfd6bf 100644 --- a/i3bar/include/util.h +++ b/i3bar/include/util.h @@ -15,7 +15,7 @@ #undef MIN #define MIN(x, y) ((x) < (y) ? (x) : (y)) -#define STARTS_WITH(string, len, needle) ((len >= strlen(needle)) && strncasecmp(string, needle, strlen(needle)) == 0) +#define STARTS_WITH(string, len, needle) (((len) >= strlen((needle))) && strncasecmp((string), (needle), strlen((needle))) == 0) /* Securely free p */ #define FREE(p) \ diff --git a/include/con.h b/include/con.h index db5fcfbf..5b48120b 100644 --- a/include/con.h +++ b/include/con.h @@ -433,3 +433,9 @@ char *con_get_tree_representation(Con *con); * */ void con_force_split_parents_redraw(Con *con); + +/** + * Returns the window title considering the current title format. + * + */ +i3String *con_parse_title_format(Con *con); diff --git a/include/data.h b/include/data.h index 9ccc2c2e..959068e0 100644 --- a/include/data.h +++ b/include/data.h @@ -376,8 +376,6 @@ struct Window { /** The name of the window. */ i3String *name; - /** The format with which the window's name should be displayed. */ - char *title_format; /** The WM_WINDOW_ROLE of this window (for example, the pidgin buddy window * sets "buddy list"). Useful to match specific windows in assignments or @@ -588,6 +586,9 @@ struct Con { char *name; + /** The format with which the window's name should be displayed. */ + char *title_format; + /* a sticky-group is an identifier which bundles several containers to a * group. The contents are shared between all of them, that is they are * displayed on whichever of the containers is currently visible */ diff --git a/include/libi3.h b/include/libi3.h index 0c69f1c0..9f6eff2b 100644 --- a/include/libi3.h +++ b/include/libi3.h @@ -504,6 +504,20 @@ char *get_config_path(const char *override_configpath, bool use_system_paths); int mkdirp(const char *path, mode_t mode); #endif +/** Helper structure for usage in format_placeholders(). */ +typedef struct placeholder_t { + /* The placeholder to be replaced, e.g., "%title". */ + char *name; + /* The value this placeholder should be replaced with. */ + char *value; +} placeholder_t; + +/** + * Replaces occurences of the defined placeholders in the format string. + * + */ +char *format_placeholders(char *format, placeholder_t *placeholders, int num); + #ifdef CAIRO_SUPPORT /* We need to flush cairo surfaces twice to avoid an assertion bug. See #1989 * and https://bugs.freedesktop.org/show_bug.cgi?id=92455. */ diff --git a/include/util.h b/include/util.h index 2667df89..28655178 100644 --- a/include/util.h +++ b/include/util.h @@ -20,7 +20,7 @@ if (pointer == NULL) \ die(__VA_ARGS__); \ } -#define STARTS_WITH(string, needle) (strncasecmp(string, needle, strlen(needle)) == 0) +#define STARTS_WITH(string, needle) (strncasecmp((string), (needle), strlen((needle))) == 0) #define CIRCLEQ_NEXT_OR_NULL(head, elm, field) (CIRCLEQ_NEXT(elm, field) != CIRCLEQ_END(head) ? CIRCLEQ_NEXT(elm, field) : NULL) #define CIRCLEQ_PREV_OR_NULL(head, elm, field) (CIRCLEQ_PREV(elm, field) != CIRCLEQ_END(head) ? CIRCLEQ_PREV(elm, field) : NULL) #define FOR_TABLE(workspace) \ diff --git a/include/window.h b/include/window.h index 395c9883..7a248277 100644 --- a/include/window.h +++ b/include/window.h @@ -81,10 +81,3 @@ void window_update_hints(i3Window *win, xcb_get_property_reply_t *prop, bool *ur * */ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style); - -/** - * Returns the window title considering the current title format. - * If no format is set, this will simply return the window's name. - * - */ -i3String *window_parse_title_format(i3Window *win); diff --git a/libi3/format_placeholders.c b/libi3/format_placeholders.c new file mode 100644 index 00000000..825cab5c --- /dev/null +++ b/libi3/format_placeholders.c @@ -0,0 +1,67 @@ +/* + * vim:ts=4:sw=4:expandtab + * + * i3 - an improved dynamic tiling window manager + * © 2009 Michael Stapelberg and contributors (see also: LICENSE) + * + */ +#include +#include +#include + +#include "libi3.h" + +#ifndef STARTS_WITH +#define STARTS_WITH(string, needle) (strncasecmp((string), (needle), strlen((needle))) == 0) +#endif + +/* + * Replaces occurences of the defined placeholders in the format string. + * + */ +char *format_placeholders(char *format, placeholder_t *placeholders, int num) { + if (format == NULL) + return NULL; + + /* We have to first iterate over the string to see how much buffer space + * we need to allocate. */ + int buffer_len = strlen(format) + 1; + for (char *walk = format; *walk != '\0'; walk++) { + for (int i = 0; i < num; i++) { + if (!STARTS_WITH(walk, placeholders[i].name)) + continue; + + buffer_len = buffer_len - strlen(placeholders[i].name) + strlen(placeholders[i].value); + walk += strlen(placeholders[i].name) - 1; + break; + } + } + + /* Now we can parse the format string. */ + char buffer[buffer_len]; + char *outwalk = buffer; + for (char *walk = format; *walk != '\0'; walk++) { + if (*walk != '%') { + *(outwalk++) = *walk; + continue; + } + + bool matched = false; + for (int i = 0; i < num; i++) { + if (!STARTS_WITH(walk, placeholders[i].name)) { + continue; + } + + matched = true; + outwalk += sprintf(outwalk, "%s", placeholders[i].value); + walk += strlen(placeholders[i].name) - 1; + break; + } + + if (!matched) + *(outwalk++) = *walk; + } + + *outwalk = '\0'; + return sstrdup(buffer); +} diff --git a/src/commands.c b/src/commands.c index e9e5c7dd..03ae0ae0 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1926,27 +1926,33 @@ void cmd_title_format(I3_CMD, const char *format) { owindow *current; TAILQ_FOREACH(current, &owindows, owindows) { - if (current->con->window == NULL) - continue; - DLOG("setting title_format for %p / %s\n", current->con, current->con->name); - FREE(current->con->window->title_format); + FREE(current->con->title_format); /* If we only display the title without anything else, we can skip the parsing step, * so we remove the title format altogether. */ if (strcasecmp(format, "%title") != 0) { - current->con->window->title_format = sstrdup(format); + current->con->title_format = sstrdup(format); - i3String *formatted_title = window_parse_title_format(current->con->window); - ewmh_update_visible_name(current->con->window->id, i3string_as_utf8(formatted_title)); - I3STRING_FREE(formatted_title); + if (current->con->window != NULL) { + i3String *formatted_title = con_parse_title_format(current->con); + ewmh_update_visible_name(current->con->window->id, i3string_as_utf8(formatted_title)); + I3STRING_FREE(formatted_title); + } } else { - /* We can remove _NET_WM_VISIBLE_NAME since we don't display a custom title. */ - ewmh_update_visible_name(current->con->window->id, NULL); + if (current->con->window != NULL) { + /* We can remove _NET_WM_VISIBLE_NAME since we don't display a custom title. */ + ewmh_update_visible_name(current->con->window->id, NULL); + } } - /* Make sure the window title is redrawn immediately. */ - current->con->window->name_x_changed = true; + if (current->con->window != NULL) { + /* Make sure the window title is redrawn immediately. */ + current->con->window->name_x_changed = true; + } else { + /* For windowless containers we also need to force the redrawing. */ + FREE(current->con->deco_render_params); + } } cmd_output->needs_tree_render = true; diff --git a/src/con.c b/src/con.c index 24809efb..ccc8445e 100644 --- a/src/con.c +++ b/src/con.c @@ -2000,3 +2000,47 @@ char *con_get_tree_representation(Con *con) { return complete_buf; } + +/* + * Returns the container's title considering the current title format. + * + */ +i3String *con_parse_title_format(Con *con) { + assert(con->title_format != NULL); + + i3Window *win = con->window; + + /* We need to ensure that we only escape the window title if pango + * is used by the current font. */ + const bool pango_markup = font_is_pango(); + + char *title; + char *class; + char *instance; + if (win == NULL) { + title = pango_escape_markup(con_get_tree_representation(con)); + class = sstrdup("i3-frame"); + instance = sstrdup("i3-frame"); + } else { + title = pango_escape_markup(sstrdup((win->name == NULL) ? "" : i3string_as_utf8(win->name))); + class = pango_escape_markup(sstrdup((win->class_class == NULL) ? "" : win->class_class)); + instance = pango_escape_markup(sstrdup((win->class_instance == NULL) ? "" : win->class_instance)); + } + + placeholder_t placeholders[] = { + {.name = "%title", .value = title}, + {.name = "%class", .value = class}, + {.name = "%instance", .value = instance}}; + const size_t num = sizeof(placeholders) / sizeof(placeholder_t); + + char *formatted_str = format_placeholders(con->title_format, &placeholders[0], num); + i3String *formatted = i3string_from_utf8(formatted_str); + i3string_set_markup(formatted, pango_markup); + FREE(formatted_str); + + for (size_t i = 0; i < num; i++) { + FREE(placeholders[i].value); + } + + return formatted; +} diff --git a/src/ipc.c b/src/ipc.c index 276a737a..00b468fb 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -372,6 +372,11 @@ void dump_node(yajl_gen gen, struct Con *con, bool inplace_restart) { else y(null); + if (con->title_format != NULL) { + ystr("title_format"); + ystr(con->title_format); + } + if (con->type == CT_WORKSPACE) { ystr("num"); y(integer, con->num); diff --git a/src/load_layout.c b/src/load_layout.c index d9acd1ba..9856d078 100644 --- a/src/load_layout.c +++ b/src/load_layout.c @@ -264,6 +264,9 @@ static int json_string(void *ctx, const unsigned char *val, size_t len) { if (strcasecmp(last_key, "name") == 0) { json_node->name = scalloc(len + 1, 1); memcpy(json_node->name, val, len); + } else if (strcasecmp(last_key, "title_format") == 0) { + json_node->title_format = scalloc(len + 1, 1); + memcpy(json_node->title_format, val, len); } else if (strcasecmp(last_key, "sticky_group") == 0) { json_node->sticky_group = scalloc(len + 1, 1); memcpy(json_node->sticky_group, val, len); diff --git a/src/util.c b/src/util.c index 67dc5c92..35ce8b19 100644 --- a/src/util.c +++ b/src/util.c @@ -343,6 +343,7 @@ char *pango_escape_markup(char *input) { char *escaped = g_markup_escape_text(input, -1); FREE(input); + return escaped; } diff --git a/src/window.c b/src/window.c index f787e078..a86f77a2 100644 --- a/src/window.c +++ b/src/window.c @@ -67,8 +67,9 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool befo win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), xcb_get_property_value_length(prop)); - if (win->title_format != NULL) { - i3String *name = window_parse_title_format(win); + Con *con = con_by_window_id(win->id); + if (con != NULL && con->title_format != NULL) { + i3String *name = con_parse_title_format(con); ewmh_update_visible_name(win->id, i3string_as_utf8(name)); I3STRING_FREE(name); } @@ -110,8 +111,10 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bo i3string_free(win->name); win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), xcb_get_property_value_length(prop)); - if (win->title_format != NULL) { - i3String *name = window_parse_title_format(win); + + Con *con = con_by_window_id(win->id); + if (con != NULL && con->title_format != NULL) { + i3String *name = con_parse_title_format(con); ewmh_update_visible_name(win->id, i3string_as_utf8(name)); I3STRING_FREE(name); } @@ -340,77 +343,3 @@ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, bo #undef MWM_DECOR_BORDER #undef MWM_DECOR_TITLE } - -/* - * Returns the window title considering the current title format. - * If no format is set, this will simply return the window's name. - * - */ -i3String *window_parse_title_format(i3Window *win) { - char *format = win->title_format; - if (format == NULL) - return i3string_copy(win->name); - - /* We initialize these lazily so we only escape them if really necessary. */ - char *escaped_title = NULL; - char *escaped_class = NULL; - char *escaped_instance = NULL; - - /* We have to first iterate over the string to see how much buffer space - * we need to allocate. */ - int buffer_len = strlen(format) + 1; - for (char *walk = format; *walk != '\0'; walk++) { - if (STARTS_WITH(walk, "%title")) { - if (escaped_title == NULL) - escaped_title = pango_escape_markup(sstrdup((win->name == NULL) ? "" : i3string_as_utf8(win->name))); - - buffer_len = buffer_len - strlen("%title") + strlen(escaped_title); - walk += strlen("%title") - 1; - } else if (STARTS_WITH(walk, "%class")) { - if (escaped_class == NULL) - escaped_class = pango_escape_markup(sstrdup((win->class_class == NULL) ? "" : win->class_class)); - - buffer_len = buffer_len - strlen("%class") + strlen(escaped_class); - walk += strlen("%class") - 1; - } else if (STARTS_WITH(walk, "%instance")) { - if (escaped_instance == NULL) - escaped_instance = pango_escape_markup(sstrdup((win->class_instance == NULL) ? "" : win->class_instance)); - - buffer_len = buffer_len - strlen("%instance") + strlen(escaped_instance); - walk += strlen("%instance") - 1; - } - } - - /* Now we can parse the format string. */ - char buffer[buffer_len]; - char *outwalk = buffer; - for (char *walk = format; *walk != '\0'; walk++) { - if (*walk != '%') { - *(outwalk++) = *walk; - continue; - } - - if (STARTS_WITH(walk + 1, "title")) { - outwalk += sprintf(outwalk, "%s", escaped_title); - walk += strlen("title"); - } else if (STARTS_WITH(walk + 1, "class")) { - outwalk += sprintf(outwalk, "%s", escaped_class); - walk += strlen("class"); - } else if (STARTS_WITH(walk + 1, "instance")) { - outwalk += sprintf(outwalk, "%s", escaped_instance); - walk += strlen("instance"); - } else { - *(outwalk++) = *walk; - } - } - *outwalk = '\0'; - - i3String *formatted = i3string_from_utf8(buffer); - i3string_set_markup(formatted, font_is_pango()); - - FREE(escaped_title); - FREE(escaped_class); - FREE(escaped_instance); - - return formatted; -} diff --git a/src/x.c b/src/x.c index 51b66c5d..c4cae170 100644 --- a/src/x.c +++ b/src/x.c @@ -528,20 +528,23 @@ void x_draw_decoration(Con *con) { struct Window *win = con->window; if (win == NULL) { - /* we have a split container which gets a representation - * of its children as title - */ - char *_title; - char *tree = con_get_tree_representation(con); - sasprintf(&_title, "i3: %s", tree); - free(tree); + i3String *title; + if (con->title_format == NULL) { + char *_title; + char *tree = con_get_tree_representation(con); + sasprintf(&_title, "i3: %s", tree); + free(tree); + + title = i3string_from_utf8(_title); + FREE(_title); + } else { + title = con_parse_title_format(con); + } - i3String *title = i3string_from_utf8(_title); draw_util_text(title, &(parent->frame_buffer), p->color->text, p->color->background, con->deco_rect.x + 2, con->deco_rect.y + text_offset_y, con->deco_rect.width - 2); - FREE(_title); I3STRING_FREE(title); goto after_title; @@ -599,12 +602,12 @@ void x_draw_decoration(Con *con) { FREE(formatted_mark); } - i3String *title = win->title_format == NULL ? win->name : window_parse_title_format(win); + i3String *title = con->title_format == NULL ? win->name : con_parse_title_format(con); draw_util_text(title, &(parent->frame_buffer), p->color->text, p->color->background, con->deco_rect.x + logical_px(2) + indent_px, con->deco_rect.y + text_offset_y, con->deco_rect.width - logical_px(2) - indent_px - mark_width - logical_px(2)); - if (win->title_format != NULL) + if (con->title_format != NULL) I3STRING_FREE(title); after_title: