From 5fcda70d0c0f2549461750c018b620a8c209ed4f Mon Sep 17 00:00:00 2001
From: George Nachman <gnachman@gmail.com>
Date: Fri, 7 Feb 2025 13:24:57 -0800
Subject: [PATCH] Add support for popups to control mode

---
 cmd-display-menu.c | 14 ++++++--
 control-notify.c   | 16 +++++++++
 control.c          | 87 +++++++++++++++++++++++++++++++++++++---------
 popup.c            | 64 ++++++++++++++++++++++++++++++----
 tmux.h             |  5 ++-
 5 files changed, 160 insertions(+), 26 deletions(-)

diff --git a/cmd-display-menu.c b/cmd-display-menu.c
index 5e742ce1..37fbbaed 100644
--- a/cmd-display-menu.c
+++ b/cmd-display-menu.c
@@ -432,7 +432,15 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
 		w = tty->sx;
 	if (h > tty->sy)
 		h = tty->sy;
-	if (!cmd_display_menu_get_position(tc, item, args, &px, &py, w, h))
+	if (tc->flags & CLIENT_CONTROL) {
+		/* Control clients may not have a window size, so provide a reasonable default so popups can still work. */
+		if (w == 0)
+			w = 80;
+		if (h == 0)
+			h = 25;
+                px = 0;
+                py = 0;
+	} else if (!cmd_display_menu_get_position(tc, item, args, &px, &py, w, h))
 		return (CMD_RETURN_NORMAL);
 
 	value = args_get(args, 'b');
@@ -485,7 +493,7 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
 	else if (args_has(args, 'E'))
 		flags |= POPUP_CLOSEEXIT;
 	if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc,
-	    argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0) {
+	    argv, cwd, title, tc, s, style, border_style, NULL, NULL, target->wp) != 0) {
 		cmd_free_argv(argc, argv);
 		if (env != NULL)
 			environ_free(env);
@@ -498,5 +506,7 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
 	free(cwd);
 	free(title);
 	cmd_free_argv(argc, argv);
+	if (tc->flags & CLIENT_CONTROL)
+		return (CMD_RETURN_NORMAL);
 	return (CMD_RETURN_WAIT);
 }
diff --git a/control-notify.c b/control-notify.c
index 30f94194..ab3ad3cc 100644
--- a/control-notify.c
+++ b/control-notify.c
@@ -260,3 +260,19 @@ control_notify_paste_buffer_deleted(const char *name)
 		control_write(c, "%%paste-buffer-deleted %s", name);
 	}
 }
+
+void
+control_notify_popup(struct client *c, int status, char *buf, size_t len, int wp)
+{
+	struct evbuffer *message = evbuffer_new();
+
+	if (message == NULL)
+		fatalx("out of memory");
+	evbuffer_add_printf(message, "%%popup %d", status);
+	if (wp != -1)
+		evbuffer_add_printf(message, " %u", wp);
+	evbuffer_add_printf(message, " : ");
+	control_escape(message, buf, len);
+	control_write_buffer(c, message);
+	evbuffer_free(message);
+}
diff --git a/control.c b/control.c
index 578d04cb..d5a1a5a4 100644
--- a/control.c
+++ b/control.c
@@ -43,7 +43,9 @@
  */
 struct control_block {
 	size_t				 size;
+        /* exactly one of `line` and `buffer` will be nonnull */
 	char				*line;
+	struct evbuffer			*buffer;
 	uint64_t			 t;
 
 	TAILQ_ENTRY(control_block)	 entry;
@@ -225,7 +227,10 @@ control_free_sub(struct control_state *cs, struct control_sub *csub)
 static void
 control_free_block(struct control_state *cs, struct control_block *cb)
 {
-	free(cb->line);
+	if (cb->line != NULL)
+		free(cb->line);
+	if (cb->buffer != NULL)
+		evbuffer_free(cb->buffer);
 	TAILQ_REMOVE(&cs->all_blocks, cb, all_entry);
 	free(cb);
 }
@@ -401,13 +406,63 @@ control_vwrite(struct client *c, const char *fmt, va_list ap)
 	free(s);
 }
 
+static void
+control_vwrite_buffer(struct client *c, struct evbuffer *buffer)
+{
+	struct control_state	*cs = c->control_state;
+
+	log_debug("%s: %s: writing buffer", __func__, c->name);
+
+	bufferevent_write_buffer(cs->write_event, buffer);
+	bufferevent_write(cs->write_event, "\n", 1);
+
+	bufferevent_enable(cs->write_event, EV_WRITE);
+}
+
+/* Frees line and buffer after using them asynchronously. */
+static void
+control_enqueue(struct client *c, struct control_state *cs, char *line, struct evbuffer *buffer)
+{
+	struct control_block *cb = xcalloc(1, sizeof *cb);
+
+	if (line != NULL) {
+		log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
+		cb->line = line;
+	} else {
+		log_debug("%s: %s: storing buffer", __func__, c->name);
+		cb->buffer = buffer;
+	}
+
+	TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
+	cb->t = get_timer();
+
+	bufferevent_enable(cs->write_event, EV_WRITE);
+}
+
+void
+control_write_buffer(struct client *c, struct evbuffer *buffer)
+{
+	struct control_state	*cs = c->control_state;
+	struct control_block	*cb;
+	va_list			 ap;
+
+	if (TAILQ_EMPTY(&cs->all_blocks)) {
+		control_vwrite_buffer(c, buffer);
+		return;
+	}
+
+	control_enqueue(c, cs, NULL, buffer);
+
+	va_end(ap);
+}
+
 /* Write a line. */
 void
 control_write(struct client *c, const char *fmt, ...)
 {
 	struct control_state	*cs = c->control_state;
-	struct control_block	*cb;
 	va_list			 ap;
+	char			*line;
 
 	va_start(ap, fmt);
 
@@ -417,13 +472,8 @@ control_write(struct client *c, const char *fmt, ...)
 		return;
 	}
 
-	cb = xcalloc(1, sizeof *cb);
-	xvasprintf(&cb->line, fmt, ap);
-	TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
-	cb->t = get_timer();
-
-	log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
-	bufferevent_enable(cs->write_event, EV_WRITE);
+	xvasprintf(&line, fmt, ap);
+	control_enqueue(c, cs, line, NULL);
 
 	va_end(ap);
 }
@@ -604,6 +654,17 @@ control_flush_all_blocks(struct client *c)
 	}
 }
 
+void
+control_escape(struct evbuffer *message, char *s, size_t size)
+{
+	for (size_t i = 0; i < size; i++) {
+		if (s[i] < ' ' || s[i] == '\\')
+			evbuffer_add_printf(message, "\\%03o", s[i]);
+		else
+			evbuffer_add_printf(message, "%c", s[i]);
+	}
+}
+
 /* Append data to buffer. */
 static struct evbuffer *
 control_append_data(struct client *c, struct control_pane *cp, uint64_t age,
@@ -611,7 +672,6 @@ control_append_data(struct client *c, struct control_pane *cp, uint64_t age,
 {
 	u_char	*new_data;
 	size_t	 new_size;
-	u_int	 i;
 
 	if (message == NULL) {
 		message = evbuffer_new();
@@ -628,12 +688,7 @@ control_append_data(struct client *c, struct control_pane *cp, uint64_t age,
 	new_data = window_pane_get_new_data(wp, &cp->offset, &new_size);
 	if (new_size < size)
 		fatalx("not enough data: %zu < %zu", new_size, size);
-	for (i = 0; i < size; i++) {
-		if (new_data[i] < ' ' || new_data[i] == '\\')
-			evbuffer_add_printf(message, "\\%03o", new_data[i]);
-		else
-			evbuffer_add_printf(message, "%c", new_data[i]);
-	}
+	control_escape(message, new_data, size);
 	window_pane_update_used_data(wp, &cp->offset, size);
 	return (message);
 }
diff --git a/popup.c b/popup.c
index 4cd147e1..ceb935b5 100644
--- a/popup.c
+++ b/popup.c
@@ -28,6 +28,7 @@
 
 struct popup_data {
 	struct client		 *c;
+	int			  wp;	
 	struct cmdq_item	 *item;
 	int			  flags;
 	char			 *title;
@@ -614,6 +615,47 @@ popup_job_update_cb(struct job *job)
 	evbuffer_drain(evb, size);
 }
 
+// NOTE TO REVIEWER: This is a copy of cmd_capture_pane_append. I think we'd want a shared implementation but I don't know where it should go.
+static char *
+popup_append(char *buf, size_t *len, char *line, size_t linelen)
+{
+	buf = xrealloc(buf, *len + linelen + 1);
+	memcpy(buf + *len, line, linelen);
+	*len += linelen;
+	return (buf);
+}
+
+static void
+popup_notify_control(struct client *c, int status, struct screen *s, int wp)
+{
+	char			*buf = NULL;
+	struct grid_cell	*gc = NULL;
+	int			 sx = screen_size_x(s);
+	int			 sy = screen_size_y(s);
+	char			*line;
+	size_t			 linelen;
+	size_t			 len = 0;
+	int			 i;
+	struct grid		*gd = s->grid;
+	const struct grid_line	*gl;
+
+	for (i = 0; i < sy; i++) {
+		line = grid_string_cells(gd, 0, i, sx, &gc, GRID_STRING_WITH_SEQUENCES, s);
+		linelen = strlen(line);
+
+		buf = popup_append(buf, &len, line, linelen);
+
+		gl = grid_peek_line(gd, i);
+		if (!(gl->flags & GRID_LINE_WRAPPED))
+			buf[len++] = '\n';
+
+		free(line);
+	}
+
+	control_notify_popup(c, status, buf, len, wp);
+	free(buf);
+}
+
 static void
 popup_job_complete_cb(struct job *job)
 {
@@ -629,8 +671,10 @@ popup_job_complete_cb(struct job *job)
 		pd->status = 0;
 	pd->job = NULL;
 
-	if ((pd->flags & POPUP_CLOSEEXIT) ||
-	    ((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
+	if (pd->c->flags & CLIENT_CONTROL)
+		popup_notify_control(pd->c, pd->status, &pd->s, pd->wp);
+	else if ((pd->flags & POPUP_CLOSEEXIT) ||
+		 ((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
 		server_client_clear_overlay(pd->c);
 }
 
@@ -639,7 +683,7 @@ popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
     u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd,
     int argc, char **argv, const char *cwd, const char *title, struct client *c,
     struct session *s, const char *style, const char *border_style,
-    popup_close_cb cb, void *arg)
+    popup_close_cb cb, void *arg, struct window_pane *wp)
 {
 	struct popup_data	*pd;
 	u_int			 jx, jy;
@@ -664,10 +708,14 @@ popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
 		jx = sx - 2;
 		jy = sy - 2;
 	}
-	if (c->tty.sx < sx || c->tty.sy < sy)
+	if (!(c->flags & CLIENT_CONTROL) && (c->tty.sx < sx || c->tty.sy < sy))
 		return (-1);
 
 	pd = xcalloc(1, sizeof *pd);
+	if (wp != NULL)
+		pd->wp = wp->id;
+	else
+		pd->wp = -1;
 	pd->item = item;
 	pd->flags = flags;
 	if (title != NULL)
@@ -723,8 +771,10 @@ popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
 	    JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy);
 	pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette);
 
-	server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
-	    popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
+	if (!(c->flags & CLIENT_CONTROL)) {
+		server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
+		    popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
+	}
 	return (0);
 }
 
@@ -811,7 +861,7 @@ popup_editor(struct client *c, const char *buf, size_t len,
 	xasprintf(&cmd, "%s %s", editor, path);
 	if (popup_display(POPUP_INTERNAL|POPUP_CLOSEEXIT, BOX_LINES_DEFAULT,
 	    NULL, px, py, sx, sy, NULL, cmd, 0, NULL, _PATH_TMP, NULL, c, NULL,
-	    NULL, NULL, popup_editor_close_cb, pe) != 0) {
+	    NULL, NULL, popup_editor_close_cb, pe, NULL) != 0) {
 		popup_editor_free(pe);
 		free(cmd);
 		return (-1);
diff --git a/tmux.h b/tmux.h
index d448faa3..9447f03a 100644
--- a/tmux.h
+++ b/tmux.h
@@ -3379,11 +3379,13 @@ struct window_pane_offset *control_pane_offset(struct client *,
 	   struct window_pane *, int *);
 void	control_reset_offsets(struct client *);
 void printflike(2, 3) control_write(struct client *, const char *, ...);
+void	control_write_buffer(struct client *c, struct evbuffer *buffer);
 void	control_write_output(struct client *, struct window_pane *);
 int	control_all_done(struct client *);
 void	control_add_sub(struct client *, const char *, enum control_sub_type,
     	   int, const char *);
 void	control_remove_sub(struct client *, const char *);
+void	control_escape(struct evbuffer *, char *, size_t);
 
 /* control-notify.c */
 void	control_notify_pane_mode_changed(int);
@@ -3400,6 +3402,7 @@ void	control_notify_session_closed(struct session *);
 void	control_notify_session_window_changed(struct session *);
 void	control_notify_paste_buffer_changed(const char *);
 void	control_notify_paste_buffer_deleted(const char *);
+void	control_notify_popup(struct client *c, int status, char *buf, size_t len, int wp);
 
 /* session.c */
 extern struct sessions sessions;
@@ -3531,7 +3534,7 @@ int		 popup_display(int, enum box_lines, struct cmdq_item *, u_int,
                     u_int, u_int, u_int, struct environ *, const char *, int,
                     char **, const char *, const char *, struct client *,
                     struct session *, const char *, const char *,
-                    popup_close_cb, void *);
+                    popup_close_cb, void *, struct window_pane *);
 int		 popup_editor(struct client *, const char *, size_t,
 		    popup_finish_edit_cb, void *);