From c586208991e4291450757e3a19739f368aecbe5d Mon Sep 17 00:00:00 2001
From: nicm <nicm>
Date: Fri, 5 Jun 2020 07:33:57 +0000
Subject: [PATCH] Add support for pausing a pane when the output buffered for a
 control mode client gets too far behind. The pause-after flag with a time is
 set on the pane with refresh-client -f and a paused pane may be resumed with
 refresh-client -A. GitHub issue 2217.

---
 cmd-refresh-client.c |  4 ++-
 control.c            | 63 +++++++++++++++++++++++++++++++++++---------
 resize.c             | 16 +++++++----
 server-client.c      | 40 +++++++++++++++++++++-------
 tmux.1               | 45 ++++++++++++++++++++++++-------
 tmux.h               |  9 +++++--
 6 files changed, 138 insertions(+), 39 deletions(-)

diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c
index 4eb8417b..bbe0c736 100644
--- a/cmd-refresh-client.c
+++ b/cmd-refresh-client.c
@@ -66,6 +66,8 @@ cmd_refresh_client_update_offset(struct client *tc, const char *value)
 		control_set_pane_on(tc, wp);
 	else if (strcmp(colon, "off") == 0)
 		control_set_pane_off(tc, wp);
+	else if (strcmp(colon, "continue") == 0)
+		control_continue_pane(tc, wp);
 
 out:
 	free(copy);
@@ -168,7 +170,7 @@ cmd_refresh_client_exec(struct cmd *self, struct cmdq_item *item)
 		}
 		tty_set_size(&tc->tty, x, y, 0, 0);
 		tc->flags |= CLIENT_SIZECHANGED;
-		recalculate_sizes();
+		recalculate_sizes_now(1);
 		return (CMD_RETURN_NORMAL);
 	}
 
diff --git a/control.c b/control.c
index 1801bd75..b9b22567 100644
--- a/control.c
+++ b/control.c
@@ -65,6 +65,7 @@ struct control_pane {
 
 	int				 flags;
 #define CONTROL_PANE_OFF 0x1
+#define CONTROL_PANE_PAUSED 0x2
 
 	int				 pending_flag;
 	TAILQ_ENTRY(control_pane)	 pending_entry;
@@ -153,6 +154,19 @@ control_add_pane(struct client *c, struct window_pane *wp)
 	return (cp);
 }
 
+/* Discard output for a pane. */
+static void
+control_discard_pane(struct client *c, struct control_pane *cp)
+{
+	struct control_state	*cs = c->control_state;
+	struct control_block	*cb, *cb1;
+
+	TAILQ_FOREACH_SAFE(cb, &cp->blocks, entry, cb1) {
+		TAILQ_REMOVE(&cp->blocks, cb, entry);
+		control_free_block(cs, cb);
+	}
+}
+
 /* Get actual pane for this client. */
 static struct window_pane *
 control_window_pane(struct client *c, u_int pane)
@@ -197,7 +211,7 @@ control_pane_offset(struct client *c, struct window_pane *wp, int *off)
 	}
 
 	cp = control_get_pane(c, wp);
-	if (cp == NULL) {
+	if (cp == NULL || (cp->flags & CONTROL_PANE_PAUSED)) {
 		*off = 0;
 		return (NULL);
 	}
@@ -216,7 +230,7 @@ control_set_pane_on(struct client *c, struct window_pane *wp)
 	struct control_pane	*cp;
 
 	cp = control_get_pane(c, wp);
-	if (cp != NULL) {
+	if (cp != NULL && (cp->flags & CONTROL_PANE_OFF)) {
 		cp->flags &= ~CONTROL_PANE_OFF;
 		memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
 		memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
@@ -233,6 +247,21 @@ control_set_pane_off(struct client *c, struct window_pane *wp)
 	cp->flags |= CONTROL_PANE_OFF;
 }
 
+/* Continue a paused pane. */
+void
+control_continue_pane(struct client *c, struct window_pane *wp)
+{
+	struct control_pane	*cp;
+
+	cp = control_get_pane(c, wp);
+	if (cp != NULL && (cp->flags & CONTROL_PANE_PAUSED)) {
+		cp->flags &= ~CONTROL_PANE_PAUSED;
+		memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
+		memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
+		control_write(c, "%%continue %%%u", wp->id);
+	}
+}
+
 /* Write a line. */
 static void
 control_vwrite(struct client *c, const char *fmt, va_list ap)
@@ -285,6 +314,7 @@ control_write_output(struct client *c, struct window_pane *wp)
 	struct control_pane	*cp;
 	struct control_block	*cb;
 	size_t			 new_size;
+	uint64_t		 t;
 
 	if (winlink_find_by_window(&c->session->windows, wp->window) == NULL)
 		return;
@@ -296,8 +326,22 @@ control_write_output(struct client *c, struct window_pane *wp)
 		return;
 	}
 	cp = control_add_pane(c, wp);
-	if (cp->flags & CONTROL_PANE_OFF)
+	if (cp->flags & (CONTROL_PANE_OFF|CONTROL_PANE_PAUSED))
 		goto ignore;
+	if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
+		cb = TAILQ_FIRST(&cp->blocks);
+		if (cb != NULL) {
+			t = get_timer();
+			log_debug("%s: %s: %%%u is %lld behind", __func__,
+			    c->name, wp->id, (long long)t - cb->t);
+			if (cb->t < t - c->pause_age) {
+				cp->flags |= CONTROL_PANE_PAUSED;
+				control_discard_pane(c, cp);
+				control_write(c, "%%pause %%%u", wp->id);
+				return;
+			}
+		}
+	}
 
 	window_pane_get_new_data(wp, &cp->queued, &new_size);
 	if (new_size == 0)
@@ -585,20 +629,15 @@ control_start(struct client *c)
 	}
 }
 
-/* Flush all output for a client that is detaching. */
+/* Discard all output for a client. */
 void
-control_flush(struct client *c)
+control_discard(struct client *c)
 {
 	struct control_state	*cs = c->control_state;
 	struct control_pane	*cp;
-	struct control_block	*cb, *cb1;
 
-	RB_FOREACH(cp, control_panes, &cs->panes) {
-		TAILQ_FOREACH_SAFE(cb, &cp->blocks, entry, cb1) {
-			TAILQ_REMOVE(&cp->blocks, cb, entry);
-			control_free_block(cs, cb);
-		}
-	}
+	RB_FOREACH(cp, control_panes, &cs->panes)
+		control_discard_pane(c, cp);
 }
 
 /* Stop control mode. */
diff --git a/resize.c b/resize.c
index 68717e35..d6e6dce2 100644
--- a/resize.c
+++ b/resize.c
@@ -227,7 +227,7 @@ done:
 }
 
 void
-recalculate_size(struct window *w)
+recalculate_size(struct window *w, int now)
 {
 	struct session	*s;
 	struct client	*c;
@@ -348,10 +348,10 @@ recalculate_size(struct window *w)
 		break;
 	}
 	if (w->flags & WINDOW_RESIZE) {
-		if (changed && w->new_sx == sx && w->new_sy == sy)
+		if (!now && changed && w->new_sx == sx && w->new_sy == sy)
 			changed = 0;
 	} else {
-		if (changed && w->sx == sx && w->sy == sy)
+		if (!now && changed && w->sx == sx && w->sy == sy)
 			changed = 0;
 	}
 
@@ -360,7 +360,7 @@ recalculate_size(struct window *w)
 		return;
 	}
 	log_debug("%s: @%u new size %u,%u", __func__, w->id, sx, sy);
-	if (type == WINDOW_SIZE_MANUAL)
+	if (now || type == WINDOW_SIZE_MANUAL)
 		resize_window(w, sx, sy, xpixel, ypixel);
 	else {
 		w->new_sx = sx;
@@ -375,6 +375,12 @@ recalculate_size(struct window *w)
 
 void
 recalculate_sizes(void)
+{
+	recalculate_sizes_now(0);
+}
+
+void
+recalculate_sizes_now(int now)
 {
 	struct session	*s;
 	struct client	*c;
@@ -407,5 +413,5 @@ recalculate_sizes(void)
 
 	/* Walk each window and adjust the size. */
 	RB_FOREACH(w, windows, &windows)
-		recalculate_size(w);
+		recalculate_size(w, now);
 }
diff --git a/server-client.c b/server-client.c
index e3383aab..1a02a240 100644
--- a/server-client.c
+++ b/server-client.c
@@ -1093,7 +1093,7 @@ server_client_update_latest(struct client *c)
 	w->latest = c;
 
 	if (options_get_number(w->options, "window-size") == WINDOW_SIZE_LATEST)
-		recalculate_size(w);
+		recalculate_size(w, 0);
 }
 
 /*
@@ -1541,7 +1541,7 @@ server_client_check_pane_buffer(struct window_pane *wp)
 		    __func__, c->name, wpo->used - wp->base_offset, new_size,
 		    wp->id);
 		if (new_size > SERVER_CLIENT_PANE_LIMIT) {
-			control_flush(c);
+			control_discard(c);
 			c->flags |= CLIENT_EXIT;
 		}
 		if (wpo->used < minimum)
@@ -1785,7 +1785,7 @@ server_client_check_exit(struct client *c)
 		return;
 
 	if (c->flags & CLIENT_CONTROL) {
-		control_flush(c);
+		control_discard(c);
 		if (!control_all_done(c))
 			return;
 	}
@@ -2362,6 +2362,23 @@ server_client_get_cwd(struct client *c, struct session *s)
 	return ("/");
 }
 
+/* Get control client flags. */
+static uint64_t
+server_client_control_flags(struct client *c, const char *next)
+{
+	if (strcmp(next, "pause-after") == 0) {
+		c->pause_age = 0;
+		return (CLIENT_CONTROL_PAUSEAFTER);
+	}
+	if (sscanf(next, "pause-after=%u", &c->pause_age) == 1) {
+		c->pause_age *= 1000;
+		return (CLIENT_CONTROL_PAUSEAFTER);
+	}
+	if (strcmp(next, "no-output") == 0)
+		return (CLIENT_CONTROL_NOOUTPUT);
+	return (0);
+}
+
 /* Set client flags. */
 void
 server_client_set_flags(struct client *c, const char *flags)
@@ -2376,11 +2393,10 @@ server_client_set_flags(struct client *c, const char *flags)
 		if (not)
 			next++;
 
-		flag = 0;
-		if (c->flags & CLIENT_CONTROL) {
-			if (strcmp(next, "no-output") == 0)
-				flag = CLIENT_CONTROL_NOOUTPUT;
-		}
+		if (c->flags & CLIENT_CONTROL)
+			flag = server_client_control_flags(c, next);
+		else
+			flag = 0;
 		if (strcmp(next, "read-only") == 0)
 			flag = CLIENT_READONLY;
 		else if (strcmp(next, "ignore-size") == 0)
@@ -2405,7 +2421,8 @@ server_client_set_flags(struct client *c, const char *flags)
 const char *
 server_client_get_flags(struct client *c)
 {
-	static char s[256];
+	static char	s[256];
+	char	 	tmp[32];
 
 	*s = '\0';
 	if (c->flags & CLIENT_ATTACHED)
@@ -2416,6 +2433,11 @@ server_client_get_flags(struct client *c)
 		strlcat(s, "ignore-size,", sizeof s);
 	if (c->flags & CLIENT_CONTROL_NOOUTPUT)
 		strlcat(s, "no-output,", sizeof s);
+	if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
+		xsnprintf(tmp, sizeof tmp, "pause-after=%u,",
+		    c->pause_age / 1000);
+		strlcat(s, tmp, sizeof s);
+	}
 	if (c->flags & CLIENT_READONLY)
 		strlcat(s, "read-only,", sizeof s);
 	if (c->flags & CLIENT_ACTIVEPANE)
diff --git a/tmux.1 b/tmux.1
index 68d8b3cc..4489c9ae 100644
--- a/tmux.1
+++ b/tmux.1
@@ -977,14 +977,18 @@ detaching the client, typically causing it to exit.
 sets a comma-separated list of client flags.
 The flags are:
 .Bl -tag -width Ds
-.It read-only
-the client is read-only
+.It active-pane
+the client has an independent active pane
 .It ignore-size
 the client does not affect the size of other clients
 .It no-output
 the client does not receive pane output in control mode
-.It active-pane
-the client has an independent active pane
+.It pause-after=seconds
+output is paused once the pane is
+.Ar seconds
+behind in control mode
+.It read-only
+the client is read-only
 .El
 .Pp
 A leading
@@ -1295,22 +1299,27 @@ it.
 .Fl C
 sets the width and height of a control mode client.
 .Fl A
-informs
-.Nm
-of a control mode client's interest in a pane.
+allows a control mode client to trigger actions on a pane.
 The argument is a pane ID (with leading
 .Ql % ) ,
 a colon, then one of
-.Ql on
+.Ql on ,
+.Ql off
 or
-.Ql off .
+.Ql continue .
 If
 .Ql off ,
 .Nm
 will not send output from the pane to the client and if all clients have turned
 the pane off, will stop reading from the pane.
+If
+.Ql continue ,
+.Nm
+will return to sending output to a paused pane (see the
+.Ar pause-after
+flag).
 .Fl A
-may be given multiple times.
+may be given multiple times for different panes.
 .Pp
 .Fl f
 sets a comma-separated list of client flags, see
@@ -3345,6 +3354,10 @@ Allows setting the system clipboard.
 Allows setting the cursor colour.
 .It cstyle
 Allows setting the cursor style.
+.It extkeys
+Supports extended keys.
+.It focus
+Supports focus reporting.
 .It margins
 Supports DECSLRM margins.
 .It overline
@@ -3353,6 +3366,8 @@ Supports the overline SGR attribute.
 Supports the DECFRA rectangle fill escape sequence.
 .It RGB
 Supports RGB colour with the SGR escape sequences.
+.It strikethrough
+Supports the strikethrough SGR escape sequence.
 .It sync
 Supports synchronized updates.
 .It title
@@ -5881,6 +5896,12 @@ The client is now attached to the session with ID
 .Ar session-id ,
 which is named
 .Ar name .
+.It Ic %continue Ar pane-id
+The pane has been continued after being paused (if the
+.Ar pause-after
+flag is set, see
+.Ic refresh-client
+.Fl A ) .
 .It Ic %exit Op Ar reason
 The
 .Nm
@@ -5907,6 +5928,10 @@ escapes non-printable characters and backslash as octal \\xxx.
 The pane with ID
 .Ar pane-id
 has changed mode.
+.It Ic %pause Ar pane-id
+The pane has been paused (if the
+.Ar pause-after
+flag is set).
 .It Ic %session-changed Ar session-id Ar name
 The client is now attached to the session with ID
 .Ar session-id ,
diff --git a/tmux.h b/tmux.h
index 4523fce4..fa11708c 100644
--- a/tmux.h
+++ b/tmux.h
@@ -1575,7 +1575,9 @@ struct client {
 	struct cmdq_list *queue;
 
 	struct client_windows windows;
+
 	struct control_state *control_state;
+	u_int		 pause_age;
 
 	pid_t		 pid;
 	int		 fd;
@@ -1643,6 +1645,7 @@ struct client {
 #define CLIENT_REDRAWPANES 0x20000000
 #define CLIENT_NOFORK 0x40000000
 #define CLIENT_ACTIVEPANE 0x80000000ULL
+#define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL
 #define CLIENT_ALLREDRAWFLAGS		\
 	(CLIENT_REDRAWWINDOW|		\
 	 CLIENT_REDRAWSTATUS|		\
@@ -2449,8 +2452,9 @@ void	 status_prompt_save_history(void);
 void	 resize_window(struct window *, u_int, u_int, int, int);
 void	 default_window_size(struct client *, struct session *, struct window *,
 	     u_int *, u_int *, u_int *, u_int *, int);
-void	 recalculate_size(struct window *);
+void	 recalculate_size(struct window *, int);
 void	 recalculate_sizes(void);
+void	 recalculate_sizes_now(int);
 
 /* input.c */
 struct input_ctx *input_init(struct window_pane *, struct bufferevent *);
@@ -2837,11 +2841,12 @@ char	*default_window_name(struct window *);
 char	*parse_window_name(const char *);
 
 /* control.c */
-void	control_flush(struct client *);
+void	control_discard(struct client *);
 void	control_start(struct client *);
 void	control_stop(struct client *);
 void	control_set_pane_on(struct client *, struct window_pane *);
 void	control_set_pane_off(struct client *, struct window_pane *);
+void	control_continue_pane(struct client *, struct window_pane *);
 struct window_pane_offset *control_pane_offset(struct client *,
 	   struct window_pane *, int *);
 void	control_reset_offsets(struct client *);