summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornicm <nicm>2020-07-06 09:14:20 +0000
committernicm <nicm>2020-07-06 09:14:20 +0000
commit66d5e5de7ac6a81f638d1a2b2f5262368a68fee2 (patch)
treea633b354a3514ed0fff68aea74fd806327039458
parent2bf612a8066ab75725eeb09adf1ebc0f4fe851ab (diff)
Add a way for control mode clients to subscribe to a format and be
notified of changes rather than having to poll. GitHub issue 2242.
-rw-r--r--cmd-refresh-client.c65
-rw-r--r--control.c391
-rw-r--r--server-client.c6
-rw-r--r--tmux.148
-rw-r--r--tmux.h12
5 files changed, 509 insertions, 13 deletions
diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c
index f7b6269b..e55ce907 100644
--- a/cmd-refresh-client.c
+++ b/cmd-refresh-client.c
@@ -34,27 +34,62 @@ const struct cmd_entry cmd_refresh_client_entry = {
.name = "refresh-client",
.alias = "refresh",
- .args = { "A:cC:Df:F:lLRSt:U", 0, 1 },
- .usage = "[-cDlLRSU] [-A pane:state] [-C XxY] [-f flags] "
- CMD_TARGET_CLIENT_USAGE " [adjustment]",
+ .args = { "A:B:cC:Df:F:lLRSt:U", 0, 1 },
+ .usage = "[-cDlLRSU] [-A pane:state] [-B name:what:format] "
+ "[-C XxY] [-f flags] " CMD_TARGET_CLIENT_USAGE " [adjustment]",
.flags = CMD_AFTERHOOK|CMD_CLIENT_TFLAG,
.exec = cmd_refresh_client_exec
};
static void
+cmd_refresh_client_update_subscription(struct client *tc, const char *value)
+{
+ char *copy, *split, *name, *what;
+ enum control_sub_type subtype;
+ int subid = -1;
+
+ copy = name = xstrdup(value);
+ if ((split = strchr(copy, ':')) == NULL) {
+ control_remove_sub(tc, copy);
+ goto out;
+ }
+ *split++ = '\0';
+
+ what = split;
+ if ((split = strchr(what, ':')) == NULL)
+ goto out;
+ *split++ = '\0';
+
+ if (strcmp(what, "%*") == 0)
+ subtype = CONTROL_SUB_ALL_PANES;
+ else if (sscanf(what, "%%%d", &subid) == 1 && subid >= 0)
+ subtype = CONTROL_SUB_PANE;
+ else if (strcmp(what, "@*") == 0)
+ subtype = CONTROL_SUB_ALL_WINDOWS;
+ else if (sscanf(what, "@%d", &subid) == 1 && subid >= 0)
+ subtype = CONTROL_SUB_WINDOW;
+ else
+ subtype = CONTROL_SUB_SESSION;
+ control_add_sub(tc, name, subtype, subid, split);
+
+out:
+ free(copy);
+}
+
+static void
cmd_refresh_client_update_offset(struct client *tc, const char *value)
{
struct window_pane *wp;
- char *copy, *colon;
+ char *copy, *split;
u_int pane;
if (*value != '%')
return;
copy = xstrdup(value);
- if ((colon = strchr(copy, ':')) == NULL)
+ if ((split = strchr(copy, ':')) == NULL)
goto out;
- *colon++ = '\0';
+ *split++ = '\0';
if (sscanf(copy, "%%%u", &pane) != 1)
goto out;
@@ -62,13 +97,13 @@ cmd_refresh_client_update_offset(struct client *tc, const char *value)
if (wp == NULL)
goto out;
- if (strcmp(colon, "on") == 0)
+ if (strcmp(split, "on") == 0)
control_set_pane_on(tc, wp);
- else if (strcmp(colon, "off") == 0)
+ else if (strcmp(split, "off") == 0)
control_set_pane_off(tc, wp);
- else if (strcmp(colon, "continue") == 0)
+ else if (strcmp(split, "continue") == 0)
control_continue_pane(tc, wp);
- else if (strcmp(colon, "pause") == 0)
+ else if (strcmp(split, "pause") == 0)
control_pause_pane(tc, wp);
out:
@@ -156,6 +191,16 @@ cmd_refresh_client_exec(struct cmd *self, struct cmdq_item *item)
}
return (CMD_RETURN_NORMAL);
}
+ if (args_has(args, 'B')) {
+ if (~tc->flags & CLIENT_CONTROL)
+ goto not_control_client;
+ value = args_first_value(args, 'B', &av);
+ while (value != NULL) {
+ cmd_refresh_client_update_subscription(tc, value);
+ value = args_next_value(&av);
+ }
+ return (CMD_RETURN_NORMAL);
+ }
if (args_has(args, 'C')) {
if (~tc->flags & CLIENT_CONTROL)
goto not_control_client;
diff --git a/control.c b/control.c
index 5681d2dc..c52f2020 100644
--- a/control.c
+++ b/control.c
@@ -76,6 +76,42 @@ struct control_pane {
};
RB_HEAD(control_panes, control_pane);
+/* Subscription pane. */
+struct control_sub_pane {
+ u_int pane;
+ u_int idx;
+ char *last;
+
+ RB_ENTRY(control_sub_pane) entry;
+};
+RB_HEAD(control_sub_panes, control_sub_pane);
+
+/* Subscription window. */
+struct control_sub_window {
+ u_int window;
+ u_int idx;
+ char *last;
+
+ RB_ENTRY(control_sub_window) entry;
+};
+RB_HEAD(control_sub_windows, control_sub_window);
+
+/* Control client subscription. */
+struct control_sub {
+ char *name;
+ char *format;
+
+ enum control_sub_type type;
+ u_int id;
+
+ char *last;
+ struct control_sub_panes panes;
+ struct control_sub_windows windows;
+
+ RB_ENTRY(control_sub) entry;
+};
+RB_HEAD(control_subs, control_sub);
+
/* Control client state. */
struct control_state {
struct control_panes panes;
@@ -87,6 +123,9 @@ struct control_state {
struct bufferevent *read_event;
struct bufferevent *write_event;
+
+ struct control_subs subs;
+ struct event subs_timer;
};
/* Low and high watermarks. */
@@ -116,6 +155,73 @@ control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2)
}
RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp);
+/* Compare client subs. */
+static int
+control_sub_cmp(struct control_sub *csub1, struct control_sub *csub2)
+{
+ return (strcmp(csub1->name, csub2->name));
+}
+RB_GENERATE_STATIC(control_subs, control_sub, entry, control_sub_cmp);
+
+/* Compare client subscription panes. */
+static int
+control_sub_pane_cmp(struct control_sub_pane *csp1,
+ struct control_sub_pane *csp2)
+{
+ if (csp1->pane < csp2->pane)
+ return (-1);
+ if (csp1->pane > csp2->pane)
+ return (1);
+ if (csp1->idx < csp2->idx)
+ return (-1);
+ if (csp1->idx > csp2->idx)
+ return (1);
+ return (0);
+}
+RB_GENERATE_STATIC(control_sub_panes, control_sub_pane, entry,
+ control_sub_pane_cmp);
+
+/* Compare client subscription windows. */
+static int
+control_sub_window_cmp(struct control_sub_window *csw1,
+ struct control_sub_window *csw2)
+{
+ if (csw1->window < csw2->window)
+ return (-1);
+ if (csw1->window > csw2->window)
+ return (1);
+ if (csw1->idx < csw2->idx)
+ return (-1);
+ if (csw1->idx > csw2->idx)
+ return (1);
+ return (0);
+}
+RB_GENERATE_STATIC(control_sub_windows, control_sub_window, entry,
+ control_sub_window_cmp);
+
+/* Free a subscription. */
+static void
+control_free_sub(struct control_state *cs, struct control_sub *csub)
+{
+ struct control_sub_pane *csp, *csp1;
+ struct control_sub_window *csw, *csw1;
+
+ RB_FOREACH_SAFE(csp, control_sub_panes, &csub->panes, csp1) {
+ RB_REMOVE(control_sub_panes, &csub->panes, csp);
+ free(csp);
+ }
+ RB_FOREACH_SAFE(csw, control_sub_windows, &csub->windows, csw1) {
+ RB_REMOVE(control_sub_windows, &csub->windows, csw);
+ free(csw);
+ }
+ free(csub->last);
+
+ RB_REMOVE(control_subs, &cs->subs, csub);
+ free(csub->name);
+ free(csub->format);
+ free(csub);
+}
+
/* Free a block. */
static void
control_free_block(struct control_state *cs, struct control_block *cb)
@@ -666,6 +772,7 @@ control_start(struct client *c)
RB_INIT(&cs->panes);
TAILQ_INIT(&cs->pending_list);
TAILQ_INIT(&cs->all_blocks);
+ RB_INIT(&cs->subs);
cs->read_event = bufferevent_new(c->fd, control_read_callback,
control_write_callback, control_error_callback, c);
@@ -704,14 +811,298 @@ control_stop(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_block *cb, *cb1;
+ struct control_sub *csub, *csub1;
if (~c->flags & CLIENT_CONTROLCONTROL)
bufferevent_free(cs->write_event);
bufferevent_free(cs->read_event);
+ RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1)
+ control_free_sub(cs, csub);
+ if (evtimer_initialized(&cs->subs_timer))
+ evtimer_del(&cs->subs_timer);
+
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
control_reset_offsets(c);
free(cs);
}
+
+/* Check session subscription. */
+static void
+control_check_subs_session(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct format_tree *ft;
+ char *value;
+
+ ft = format_create_defaults(NULL, c, s, NULL, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ if (csub->last != NULL && strcmp(value, csub->last) == 0) {
+ free(value);
+ return;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u - - - : %s",
+ csub->name, s->id, value);
+ free(csub->last);
+ csub->last = value;
+}
+
+/* Check pane subscription. */
+static void
+control_check_subs_pane(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window_pane *wp;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_pane *csp, find;
+
+ wp = window_pane_find_by_id(csub->id);
+ if (wp == NULL)
+ return;
+ w = wp->window;
+
+ TAILQ_FOREACH(wl, &w->winlinks, wentry) {
+ if (wl->session != s)
+ continue;
+
+ ft = format_create_defaults(NULL, c, s, wl, wp);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.pane = wp->id;
+ find.idx = wl->idx;
+
+ csp = RB_FIND(control_sub_panes, &csub->panes, &find);
+ if (csp == NULL) {
+ csp = xcalloc(1, sizeof *csp);
+ csp->pane = wp->id;
+ csp->idx = wl->idx;
+ RB_INSERT(control_sub_panes, &csub->panes, csp);
+ }
+
+ if (csp->last != NULL && strcmp(value, csp->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u %%%u : %s",
+ csub->name, s->id, w->id, wl->idx, wp->id, value);
+ free(csp->last);
+ csp->last = value;
+ }
+}
+
+/* Check all panes subscription. */
+static void
+control_check_subs_all_panes(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window_pane *wp;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_pane *csp, find;
+
+ RB_FOREACH(wl, winlinks, &s->windows) {
+ w = wl->window;
+ TAILQ_FOREACH(wp, &w->panes, entry) {
+ ft = format_create_defaults(NULL, c, s, wl, wp);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.pane = wp->id;
+ find.idx = wl->idx;
+
+ csp = RB_FIND(control_sub_panes, &csub->panes, &find);
+ if (csp == NULL) {
+ csp = xcalloc(1, sizeof *csp);
+ csp->pane = wp->id;
+ csp->idx = wl->idx;
+ RB_INSERT(control_sub_panes, &csub->panes, csp);
+ }
+
+ if (csp->last != NULL &&
+ strcmp(value, csp->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u %%%u : %s",
+ csub->name, s->id, w->id, wl->idx, wp->id, value);
+ free(csp->last);
+ csp->last = value;
+ }
+ }
+}
+
+/* Check window subscription. */
+static void
+control_check_subs_window(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_window *csw, find;
+
+ w = window_find_by_id(csub->id);
+ if (w == NULL)
+ return;
+
+ TAILQ_FOREACH(wl, &w->winlinks, wentry) {
+ if (wl->session != s)
+ continue;
+
+ ft = format_create_defaults(NULL, c, s, wl, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.window = w->id;
+ find.idx = wl->idx;
+
+ csw = RB_FIND(control_sub_windows, &csub->windows, &find);
+ if (csw == NULL) {
+ csw = xcalloc(1, sizeof *csw);
+ csw->window = w->id;
+ csw->idx = wl->idx;
+ RB_INSERT(control_sub_windows, &csub->windows, csw);
+ }
+
+ if (csw->last != NULL && strcmp(value, csw->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u - : %s",
+ csub->name, s->id, w->id, wl->idx, value);
+ free(csw->last);
+ csw->last = value;
+ }
+}
+
+/* Check all windows subscription. */
+static void
+control_check_subs_all_windows(struct client *c, struct control_sub *csub)
+{
+ struct session *s = c->session;
+ struct window *w;
+ struct winlink *wl;
+ struct format_tree *ft;
+ char *value;
+ struct control_sub_window *csw, find;
+
+ RB_FOREACH(wl, winlinks, &s->windows) {
+ w = wl->window;
+
+ ft = format_create_defaults(NULL, c, s, wl, NULL);
+ value = format_expand(ft, csub->format);
+ format_free(ft);
+
+ find.window = w->id;
+ find.idx = wl->idx;
+
+ csw = RB_FIND(control_sub_windows, &csub->windows, &find);
+ if (csw == NULL) {
+ csw = xcalloc(1, sizeof *csw);
+ csw->window = w->id;
+ csw->idx = wl->idx;
+ RB_INSERT(control_sub_windows, &csub->windows, csw);
+ }
+
+ if (csw->last != NULL && strcmp(value, csw->last) == 0) {
+ free(value);
+ continue;
+ }
+ control_write(c,
+ "%%subscription-changed %s $%u @%u %u - : %s",
+ csub->name, s->id, w->id, wl->idx, value);
+ free(csw->last);
+ csw->last = value;
+ }
+}
+
+/* Check subscriptions timer. */
+static void
+control_check_subs_timer(__unused int fd, __unused short events, void *data)
+{
+ struct client *c = data;
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, *csub1;
+ struct timeval tv = { .tv_sec = 1 };
+
+ log_debug("%s: timer fired", __func__);
+ evtimer_add(&cs->subs_timer, &tv);
+
+ RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) {
+ switch (csub->type) {
+ case CONTROL_SUB_SESSION:
+ control_check_subs_session(c, csub);
+ break;
+ case CONTROL_SUB_PANE:
+ control_check_subs_pane(c, csub);
+ break;
+ case CONTROL_SUB_ALL_PANES:
+ control_check_subs_all_panes(c, csub);
+ break;
+ case CONTROL_SUB_WINDOW:
+ control_check_subs_window(c, csub);
+ break;
+ case CONTROL_SUB_ALL_WINDOWS:
+ control_check_subs_all_windows(c, csub);
+ break;
+ }
+ }
+}
+
+/* Add a subscription. */
+void
+control_add_sub(struct client *c, const char *name, enum control_sub_type type,
+ int id, const char *format)
+{
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, find;
+ struct timeval tv = { .tv_sec = 1 };
+
+ find.name = (char *)name;
+ if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
+ control_free_sub(cs, csub);
+
+ csub = xcalloc(1, sizeof *csub);
+ csub->name = xstrdup(name);
+ csub->type = type;
+ csub->id = id;
+ csub->format = xstrdup(format);
+ RB_INSERT(control_subs, &cs->subs, csub);
+
+ RB_INIT(&csub->panes);
+ RB_INIT(&csub->windows);
+
+ if (!evtimer_initialized(&cs->subs_timer))
+ evtimer_set(&cs->subs_timer, control_check_subs_timer, c);
+ if (!evtimer_pending(&cs->subs_timer, NULL))
+ evtimer_add(&cs->subs_timer, &tv);
+}
+
+/* Remove a subscription. */
+void
+control_remove_sub(struct client *c, const char *name)
+{
+ struct control_state *cs = c->control_state;
+ struct control_sub *csub, find;
+
+ find.name = (char *)name;
+ if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
+ control_free_sub(cs, csub);
+ if (RB_EMPTY(&cs->subs))
+ evtimer_del(&cs->subs_timer);
+}
diff --git a/server-client.c b/server-client.c
index d86a9fb8..3a79a5d1 100644
--- a/server-client.c
+++ b/server-client.c
@@ -1474,11 +1474,13 @@ server_client_check_pane_resize(struct window_pane *wp)
* Otherwise resize to the force size and start the timer.
*/
if (wp->flags & PANE_RESIZENOW) {
- log_debug("%s: resizing %%%u after forced resize", __func__, wp->id);
+ log_debug("%s: resizing %%%u after forced resize",
+ __func__, wp->id);
window_pane_send_resize(wp, 0);
wp->flags &= ~(PANE_RESIZE|PANE_RESIZEFORCE|PANE_RESIZENOW);
} else if (!evtimer_pending(&wp->force_timer, NULL)) {
- log_debug("%s: forcing resize of %%%u", __func__, wp->id);
+ log_debug("%s: forcing resize of %%%u", __func__,
+ wp->id);
window_pane_send_resize(wp, 1);
server_client_start_force_timer(wp);
}
diff --git a/tmux.1 b/tmux.1
index 4d079853..1c02bdda 100644
--- a/tmux.1
+++ b/tmux.1
@@ -1255,6 +1255,7 @@ specified multiple times.
.It Xo Ic refresh-client
.Op Fl cDlLRSU
.Op Fl A Ar pane:state
+.Op Fl B Ar name:what:format
.Op Fl C Ar XxY
.Op Fl f Ar flags
.Op Fl t Ar target-client
@@ -1328,6 +1329,31 @@ will pause the pane.
.Fl A
may be given multiple times for different panes.
.Pp
+.Fl B
+sets a subscription to a format for a control mode client.
+The argument is split into three items by colons:
+.Ar name
+is a name for the subscription;
+.Ar what
+is a type of item to subscribe to;
+.Ar format
+is the format.
+After a subscription is added, changes to the format are reported with the
+.Ic %subscription-changed
+notification, at most once a second.
+If only the name is given, the subscription is removed.
+.Ar what
+may be empty to check the format only for the attached session, or one of:
+a pane ID such as
+.Ql %0 ;
+.Ql %*
+for all panes in the attached session;
+an window ID such as
+.Ql @0 ;
+or
+.Ql @*
+for all windows in the attached session.
+.Pp
.Fl f
sets a comma-separated list of client flags, see
.Ic attach-session .
@@ -5932,7 +5958,7 @@ or an error occurred.
If present,
.Ar reason
describes why the client exited.
-.It Ic %extended-output Ar pane-id Ar age Ar ... : Ar value
+.It Ic %extended-output Ar pane-id Ar age Ar ... \& : Ar value
New form of
.Ic %output
sent when the
@@ -5980,6 +6006,26 @@ changed its active window to the window with ID
.Ar window-id .
.It Ic %sessions-changed
A session was created or destroyed.
+.It Xo Ic %subscription-changed
+.Ar name
+.Ar session-id
+.Ar window-id
+.Ar window-index
+.Ar pane-id ... \& :
+.Ar value
+.Xc
+The value of the format associated with subscription
+.Ar name
+has changed to
+.Ar value .
+See
+.Ic refresh-client
+.Fl B .
+Any arguments after
+.Ar pane-id
+up until a single
+.Ql \&:
+are for future use and should be ignored.
.It Ic %unlinked-window-add Ar window-id
The window with ID
.Ar window-id
diff --git a/tmux.h b/tmux.h
index a8c67051..07dfc0ae 100644
--- a/tmux.h
+++ b/tmux.h
@@ -1722,6 +1722,15 @@ struct client {
};
TAILQ_HEAD(clients, client);
+/* Control mode subscription type. */
+enum control_sub_type {
+ CONTROL_SUB_SESSION,
+ CONTROL_SUB_PANE,
+ CONTROL_SUB_ALL_PANES,
+ CONTROL_SUB_WINDOW,
+ CONTROL_SUB_ALL_WINDOWS
+};
+
/* Key binding and key table. */
struct key_binding {
key_code key;
@@ -2862,6 +2871,9 @@ void control_reset_offsets(struct client *);
void printflike(2, 3) control_write(struct client *, const char *, ...);
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 *);
/* control-notify.c */
void control_notify_input(struct client *, struct window_pane *,