From 228c478e8d11dd14972b237574146fd0d2d6b96c Mon Sep 17 00:00:00 2001
From: Ryan Dwyer <ryandwyer1@gmail.com>
Date: Sat, 5 May 2018 12:36:50 +1000
Subject: [PATCH] Implement title_format

This implements the title_format command, with a new placeholder %shell
which gets substituted with the view type (xwayland, xdg_shell_v6 or
wl_shell).

Example config:

    for_window [title=".*"] title_format %title (class=%class instance=%instance shell=%shell)
---
 include/sway/commands.h       |   1 +
 include/sway/tree/container.h |   6 +-
 include/sway/tree/view.h      |  10 +++
 sway/commands.c               |   1 +
 sway/commands/title_format.c  |  31 ++++++++
 sway/desktop/xdg_shell_v6.c   |   3 +-
 sway/desktop/xwayland.c       |   2 +-
 sway/meson.build              |   1 +
 sway/tree/container.c         |  46 +++--------
 sway/tree/view.c              | 142 ++++++++++++++++++++++++++++++++++
 10 files changed, 201 insertions(+), 42 deletions(-)
 create mode 100644 sway/commands/title_format.c

diff --git a/include/sway/commands.h b/include/sway/commands.h
index 0732a90a..d39ac56c 100644
--- a/include/sway/commands.h
+++ b/include/sway/commands.h
@@ -144,6 +144,7 @@ sway_cmd cmd_splitt;
 sway_cmd cmd_splitv;
 sway_cmd cmd_sticky;
 sway_cmd cmd_swaybg_command;
+sway_cmd cmd_title_format;
 sway_cmd cmd_unmark;
 sway_cmd cmd_workspace;
 sway_cmd cmd_ws_auto_back_and_forth;
diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h
index b07af72c..61ab7ca1 100644
--- a/include/sway/tree/container.h
+++ b/include/sway/tree/container.h
@@ -63,7 +63,8 @@ struct sway_container {
 	 */
 	size_t id;
 
-	char *name;
+	char *name;            // The view's title (unformatted)
+	char *formatted_title; // The title displayed in the title bar
 
 	enum sway_container_type type;
 	enum sway_container_layout layout;
@@ -204,7 +205,6 @@ void container_update_title_textures(struct sway_container *container);
  */
 void container_calculate_title_height(struct sway_container *container);
 
-void container_update_title(struct sway_container *container,
-		const char *new_title);
+void container_notify_child_title_changed(struct sway_container *container);
 
 #endif
diff --git a/include/sway/tree/view.h b/include/sway/tree/view.h
index 21127ab1..9d4256f7 100644
--- a/include/sway/tree/view.h
+++ b/include/sway/tree/view.h
@@ -48,6 +48,7 @@ struct sway_view {
 
 	bool is_fullscreen;
 
+	char *title_format;
 	enum sway_container_border border;
 	int border_thickness;
 
@@ -164,6 +165,8 @@ const char *view_get_class(struct sway_view *view);
 
 const char *view_get_instance(struct sway_view *view);
 
+const char *view_get_type(struct sway_view *view);
+
 void view_configure(struct sway_view *view, double ox, double oy, int width,
 	int height);
 
@@ -207,4 +210,11 @@ void view_child_init(struct sway_view_child *child,
 
 void view_child_destroy(struct sway_view_child *child);
 
+/**
+ * Re-read the view's title property and update any relevant title bars.
+ * The force argument makes it recreate the title bars even if the title hasn't
+ * changed.
+ */
+void view_update_title(struct sway_view *view, bool force);
+
 #endif
diff --git a/sway/commands.c b/sway/commands.c
index efed7ddf..2e1cdc2c 100644
--- a/sway/commands.c
+++ b/sway/commands.c
@@ -182,6 +182,7 @@ static struct cmd_handler command_handlers[] = {
 	{ "splith", cmd_splith },
 	{ "splitt", cmd_splitt },
 	{ "splitv", cmd_splitv },
+	{ "title_format", cmd_title_format },
 };
 
 static int handler_compare(const void *_a, const void *_b) {
diff --git a/sway/commands/title_format.c b/sway/commands/title_format.c
new file mode 100644
index 00000000..a2f6e6ff
--- /dev/null
+++ b/sway/commands/title_format.c
@@ -0,0 +1,31 @@
+#define _POSIX_C_SOURCE 200809L
+#include <string.h>
+#include "sway/commands.h"
+#include "sway/config.h"
+#include "sway/tree/arrange.h"
+#include "sway/tree/view.h"
+#include "log.h"
+#include "stringop.h"
+
+struct cmd_results *cmd_title_format(int argc, char **argv) {
+	struct cmd_results *error = NULL;
+	if ((error = checkarg(argc, "title_format", EXPECTED_AT_LEAST, 1))) {
+		return error;
+	}
+	struct sway_container *container =
+		config->handler_context.current_container;
+	if (container->type != C_VIEW) {
+		return cmd_results_new(CMD_INVALID, "title_format",
+				"Only views can have a title_format");
+	}
+	struct sway_view *view = container->sway_view;
+	char *format = join_args(argv, argc);
+	if (view->title_format) {
+		free(view->title_format);
+	}
+	view->title_format = strdup(format);
+	view_update_title(view, true);
+	config_find_font_height(true);
+	arrange_root();
+	return cmd_results_new(CMD_SUCCESS, NULL, NULL);
+}
diff --git a/sway/desktop/xdg_shell_v6.c b/sway/desktop/xdg_shell_v6.c
index 5880e9a9..fcee8ce9 100644
--- a/sway/desktop/xdg_shell_v6.c
+++ b/sway/desktop/xdg_shell_v6.c
@@ -176,8 +176,7 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 	// TODO: Let floating views do whatever
 	view_update_size(view, xdg_shell_v6_view->pending_width,
 		xdg_shell_v6_view->pending_height);
-	container_update_title(view->swayc,
-			view->wlr_xdg_surface_v6->toplevel->title);
+	view_update_title(view, false);
 	view_damage(view, false);
 }
 
diff --git a/sway/desktop/xwayland.c b/sway/desktop/xwayland.c
index a1e672ce..b4eda71f 100644
--- a/sway/desktop/xwayland.c
+++ b/sway/desktop/xwayland.c
@@ -223,7 +223,7 @@ static void handle_commit(struct wl_listener *listener, void *data) {
 	view_update_size(view, xwayland_view->pending_width,
 		xwayland_view->pending_height);
 	view_damage(view, false);
-	container_update_title(view->swayc, view->wlr_xwayland_surface->title);
+	view_update_title(view, false);
 }
 
 static void handle_unmap(struct wl_listener *listener, void *data) {
diff --git a/sway/meson.build b/sway/meson.build
index 7c93b5c4..f70a8e44 100644
--- a/sway/meson.build
+++ b/sway/meson.build
@@ -60,6 +60,7 @@ sway_sources = files(
 	'commands/set.c',
 	'commands/split.c',
 	'commands/swaybg_command.c',
+	'commands/title_format.c',
 	'commands/workspace.c',
 	'commands/ws_auto_back_and_forth.c',
 
diff --git a/sway/tree/container.c b/sway/tree/container.c
index 9fb47020..e1be32d1 100644
--- a/sway/tree/container.c
+++ b/sway/tree/container.c
@@ -348,7 +348,6 @@ struct sway_container *container_view_create(struct sway_container *sibling,
 		swayc, title, sibling, sibling ? sibling->type : 0, sibling->name);
 	// Setup values
 	swayc->sway_view = sway_view;
-	container_update_title(swayc, title);
 	swayc->width = 0;
 	swayc->height = 0;
 
@@ -572,7 +571,7 @@ static void update_title_texture(struct sway_container *con,
 	if (*texture) {
 		wlr_texture_destroy(*texture);
 	}
-	if (!con->name) {
+	if (!con->formatted_title) {
 		return;
 	}
 
@@ -581,7 +580,8 @@ static void update_title_texture(struct sway_container *con,
 	int height = config->font_height * scale;
 
 	cairo_t *c = cairo_create(NULL);
-	get_text_size(c, config->font, &width, NULL, scale, false, "%s", con->name);
+	get_text_size(c, config->font, &width, NULL, scale, false,
+			"%s", con->formatted_title);
 	cairo_destroy(c);
 
 	cairo_surface_t *surface = cairo_image_surface_create(
@@ -596,7 +596,7 @@ static void update_title_texture(struct sway_container *con,
 			class->text[2], class->text[3]);
 	cairo_move_to(cairo, 0, 0);
 
-	pango_printf(cairo, config->font, scale, false, "%s", con->name);
+	pango_printf(cairo, config->font, scale, false, "%s", con->formatted_title);
 
 	cairo_surface_flush(surface);
 	unsigned char *data = cairo_image_surface_get_data(surface);
@@ -622,57 +622,31 @@ void container_update_title_textures(struct sway_container *container) {
 }
 
 void container_calculate_title_height(struct sway_container *container) {
-	if (!container->name) {
+	if (!container->formatted_title) {
 		container->title_height = 0;
 		return;
 	}
 	cairo_t *cairo = cairo_create(NULL);
 	int height;
 	get_text_size(cairo, config->font, NULL, &height, 1, false,
-			"%s", container->name);
+			"%s", container->formatted_title);
 	cairo_destroy(cairo);
 	container->title_height = height;
 }
 
-static void container_notify_child_title_changed(
-		struct sway_container *container) {
+void container_notify_child_title_changed(struct sway_container *container) {
 	if (!container || container->type != C_CONTAINER) {
 		return;
 	}
 	if (container->layout != L_TABBED && container->layout != L_STACKED) {
 		return;
 	}
-	if (container->name) {
-		free(container->name);
+	if (container->formatted_title) {
+		free(container->formatted_title);
 	}
 	// TODO: iterate children and concatenate their titles
-	container->name = strdup("");
+	container->formatted_title = strdup("");
 	container_calculate_title_height(container);
 	container_update_title_textures(container);
 	container_notify_child_title_changed(container->parent);
 }
-
-void container_update_title(struct sway_container *container,
-		const char *new_title) {
-	if (!new_title) {
-		new_title = "";
-	}
-
-	if (container->name && strcmp(container->name, new_title) == 0) {
-		return;
-	}
-
-	if (container->name) {
-		free(container->name);
-	}
-	container->name = strdup(new_title);
-	container_calculate_title_height(container);
-	container_update_title_textures(container);
-	container_notify_child_title_changed(container->parent);
-
-	size_t prev_max_height = config->font_height;
-	config_find_font_height(false);
-	if (config->font_height != prev_max_height) {
-		arrange_root();
-	}
-}
diff --git a/sway/tree/view.c b/sway/tree/view.c
index 84962306..4a01f096 100644
--- a/sway/tree/view.c
+++ b/sway/tree/view.c
@@ -1,3 +1,4 @@
+#define _POSIX_C_SOURCE 200809L
 #include <stdlib.h>
 #include <wayland-server.h>
 #include <wlr/render/wlr_renderer.h>
@@ -67,6 +68,18 @@ const char *view_get_instance(struct sway_view *view) {
 	return NULL;
 }
 
+const char *view_get_type(struct sway_view *view) {
+	switch(view->type) {
+	case SWAY_VIEW_WL_SHELL:
+		return "wl_shell";
+	case SWAY_VIEW_XDG_SHELL_V6:
+		return "xdg_shell_v6";
+	case SWAY_VIEW_XWAYLAND:
+		return "xwayland";
+	}
+	return "unknown";
+}
+
 void view_configure(struct sway_view *view, double ox, double oy, int width,
 		int height) {
 	if (view->impl->configure) {
@@ -348,6 +361,11 @@ void view_unmap(struct sway_view *view) {
 	view->swayc = NULL;
 	view->surface = NULL;
 
+	if (view->title_format) {
+		free(view->title_format);
+		view->title_format = NULL;
+	}
+
 	if (parent->type == C_OUTPUT) {
 		arrange_output(parent);
 	} else {
@@ -475,3 +493,127 @@ void view_child_destroy(struct sway_view_child *child) {
 		free(child);
 	}
 }
+
+static char *parse_title_format(struct sway_view *view) {
+	if (!view->title_format || strcmp(view->title_format, "%title") == 0) {
+		return strdup(view_get_title(view));
+	}
+	const char *title = view_get_title(view);
+	const char *class = view_get_class(view);
+	const char *instance = view_get_instance(view);
+	const char *shell = view_get_type(view);
+	size_t title_len = title ? strlen(title) : 0;
+	size_t class_len = class ? strlen(class) : 0;
+	size_t instance_len = instance ? strlen(instance) : 0;
+	size_t shell_len = shell ? strlen(shell) : 0;
+
+	// First, determine the length
+	size_t len = 0;
+	char *format = view->title_format;
+	char *next = strchr(format, '%');
+	while (next) {
+		len += next - format;
+		format = next;
+
+		if (strncmp(next, "%title", 6) == 0) {
+			len += title_len;
+			format += 6;
+		} else if (strncmp(next, "%class", 6) == 0) {
+			len += class_len;
+			format += 6;
+		} else if (strncmp(next, "%instance", 9) == 0) {
+			len += instance_len;
+			format += 9;
+		} else if (strncmp(next, "%shell", 6) == 0) {
+			len += shell_len;
+			format += 6;
+		} else {
+			++format;
+			++len;
+		}
+		next = strchr(format, '%');
+	}
+	len += strlen(format);
+
+	char *buffer = calloc(len + 1, 1);
+	if (!sway_assert(buffer, "Unable to allocate title string")) {
+		return NULL;
+	}
+
+	// Now build the title
+	format = view->title_format;
+	next = strchr(format, '%');
+	while (next) {
+		// Copy everything up to the %
+		strncat(buffer, format, next - format);
+		format = next;
+
+		if (strncmp(next, "%title", 6) == 0) {
+			if (title) {
+				strcat(buffer, title);
+			}
+			format += 6;
+		} else if (strncmp(next, "%class", 6) == 0) {
+			if (class) {
+				strcat(buffer, class);
+			}
+			format += 6;
+		} else if (strncmp(next, "%instance", 9) == 0) {
+			if (instance) {
+				strcat(buffer, instance);
+			}
+			format += 9;
+		} else if (strncmp(next, "%shell", 6) == 0) {
+			strcat(buffer, shell);
+			format += 6;
+		} else {
+			strcat(buffer, "%");
+			++format;
+		}
+		next = strchr(format, '%');
+	}
+	strcat(buffer, format);
+
+	return buffer;
+}
+
+void view_update_title(struct sway_view *view, bool force) {
+	if (!view->swayc) {
+		return;
+	}
+	const char *title = view_get_title(view);
+
+	if (!force) {
+		if (title && view->swayc->name && strcmp(title, view->swayc->name) == 0) {
+			return;
+		}
+		if (!title && !view->swayc->name) {
+			return;
+		}
+	}
+
+	if (title) {
+		if (view->swayc->name) {
+			free(view->swayc->name);
+		}
+		if (view->swayc->formatted_title) {
+			free(view->swayc->formatted_title);
+		}
+		view->swayc->name = strdup(title);
+		view->swayc->formatted_title = parse_title_format(view);
+	} else {
+		free(view->swayc->name);
+		free(view->swayc->formatted_title);
+		view->swayc->name = NULL;
+		view->swayc->formatted_title = NULL;
+	}
+	container_calculate_title_height(view->swayc);
+	container_update_title_textures(view->swayc);
+	container_notify_child_title_changed(view->swayc->parent);
+
+	size_t prev_max_height = config->font_height;
+	config_find_font_height(false);
+	if (config->font_height != prev_max_height) {
+		arrange_root();
+	}
+}