From 66d5e5de7ac6a81f638d1a2b2f5262368a68fee2 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 6 Jul 2020 09:14:20 +0000 Subject: [PATCH] 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. --- cmd-refresh-client.c | 65 +++++-- control.c | 391 +++++++++++++++++++++++++++++++++++++++++++ server-client.c | 6 +- tmux.1 | 48 +++++- tmux.h | 12 ++ 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 *,