From 28481e984b69ccf1a59d0b6fb144bf9825ca3ae3 Mon Sep 17 00:00:00 2001 From: nicm Date: Wed, 2 Jul 2025 08:13:09 +0000 Subject: [PATCH] Add sorting to W, P, L operators as well, and add some new session format variables. From Michael Grant in GitHub issue 4516. --- format.c | 371 ++++++++++++++++++++++++++++++++++++++++++++---- options-table.c | 4 +- tmux.1 | 18 ++- tmux.h | 1 + 4 files changed, 363 insertions(+), 31 deletions(-) diff --git a/format.c b/format.c index 58cba338..5f91e0ce 100644 --- a/format.c +++ b/format.c @@ -140,8 +140,8 @@ enum format_loop_sort_type { }; static struct format_loop_sort_criteria { - u_int field; - int reversed; + enum format_loop_sort_type field; + int reversed; } format_loop_sort_criteria; struct format_tree { @@ -554,6 +554,38 @@ format_cb_session_attached_list(struct format_tree *ft) return (value); } +/* Callback for session_alert. */ +static void * +format_cb_session_alert(struct format_tree *ft) +{ + struct session *s = ft->s; + struct winlink *wl; + char alerts[1024]; + int alerted = 0; + + if (s == NULL) + return (NULL); + + *alerts = '\0'; + RB_FOREACH(wl, winlinks, &s->windows) { + if ((wl->flags & WINLINK_ALERTFLAGS) == 0) + continue; + if (~alerted & wl->flags & WINLINK_ACTIVITY) { + strlcat(alerts, "#", sizeof alerts); + alerted |= WINLINK_ACTIVITY; + } + if (~alerted & wl->flags & WINLINK_BELL) { + strlcat(alerts, "!", sizeof alerts); + alerted |= WINLINK_BELL; + } + if (~alerted & wl->flags & WINLINK_SILENCE) { + strlcat(alerts, "~", sizeof alerts); + alerted |= WINLINK_SILENCE; + } + } + return (xstrdup(alerts)); +} + /* Callback for session_alerts. */ static void * format_cb_session_alerts(struct format_tree *ft) @@ -1743,6 +1775,15 @@ format_cb_keypad_flag(struct format_tree *ft) return (NULL); } +/* Callback for loop_last_flag. */ +static void * +format_cb_loop_last_flag(struct format_tree *ft) +{ + if (ft->flags & FORMAT_LAST) + return (xstrdup("1")); + return (xstrdup("0")); +} + /* Callback for mouse_all_flag. */ static void * format_cb_mouse_all_flag(struct format_tree *ft) @@ -2264,6 +2305,66 @@ format_cb_server_sessions(__unused struct format_tree *ft) return (format_printf("%u", n)); } +/* Callback for session_active. */ +static void * +format_cb_session_active(struct format_tree *ft) +{ + if (ft->s == NULL || ft->c == NULL) + return (NULL); + + if (ft->c->session == ft->s) + return (xstrdup("1")); + return (xstrdup("0")); +} + +/* Callback for session_activity_flag. */ +static void * +format_cb_session_activity_flag(struct format_tree *ft) +{ + struct winlink *wl; + + if (ft->s != NULL) { + RB_FOREACH(wl, winlinks, &ft->s->windows) { + if (ft->wl->flags & WINLINK_ACTIVITY) + return (xstrdup("1")); + return (xstrdup("0")); + } + } + return (NULL); +} + +/* Callback for session_bell_flag. */ +static void * +format_cb_session_bell_flag(struct format_tree *ft) +{ + struct winlink *wl; + + if (ft->s != NULL) { + RB_FOREACH(wl, winlinks, &ft->s->windows) { + if (wl->flags & WINLINK_BELL) + return (xstrdup("1")); + return (xstrdup("0")); + } + } + return (NULL); +} + +/* Callback for session_silence_flag. */ +static void * +format_cb_session_silence_flag(struct format_tree *ft) +{ + struct winlink *wl; + + if (ft->s != NULL) { + RB_FOREACH(wl, winlinks, &ft->s->windows) { + if (ft->wl->flags & WINLINK_SILENCE) + return (xstrdup("1")); + return (xstrdup("0")); + } + } + return (NULL); +} + /* Callback for session_attached. */ static void * format_cb_session_attached(struct format_tree *ft) @@ -3050,6 +3151,9 @@ static const struct format_table_entry format_table[] = { { "last_window_index", FORMAT_TABLE_STRING, format_cb_last_window_index }, + { "loop_last_flag", FORMAT_TABLE_STRING, + format_cb_loop_last_flag + }, { "mouse_all_flag", FORMAT_TABLE_STRING, format_cb_mouse_all_flag }, @@ -3230,9 +3334,18 @@ static const struct format_table_entry format_table[] = { { "server_sessions", FORMAT_TABLE_STRING, format_cb_server_sessions }, + { "session_active", FORMAT_TABLE_STRING, + format_cb_session_active + }, { "session_activity", FORMAT_TABLE_TIME, format_cb_session_activity }, + { "session_activity_flag", FORMAT_TABLE_STRING, + format_cb_session_activity_flag + }, + { "session_alert", FORMAT_TABLE_STRING, + format_cb_session_alert + }, { "session_alerts", FORMAT_TABLE_STRING, format_cb_session_alerts }, @@ -3242,6 +3355,9 @@ static const struct format_table_entry format_table[] = { { "session_attached_list", FORMAT_TABLE_STRING, format_cb_session_attached_list }, + { "session_bell_flag", FORMAT_TABLE_STRING, + format_cb_session_bell_flag + }, { "session_created", FORMAT_TABLE_TIME, format_cb_session_created }, @@ -3287,6 +3403,9 @@ static const struct format_table_entry format_table[] = { { "session_path", FORMAT_TABLE_STRING, format_cb_session_path }, + { "session_silence_flag", FORMAT_TABLE_STRING, + format_cb_session_silence_flag + }, { "session_stack", FORMAT_TABLE_STRING, format_cb_session_stack }, @@ -4040,7 +4159,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("mCNSst=pReq", cp[0]) == NULL) + if (strchr("mCLNPSst=pReqW", cp[0]) == NULL) break; c = cp[0]; @@ -4239,13 +4358,14 @@ format_session_name(struct format_expand_state *es, const char *fmt) static int format_cmp_session(const void *a0, const void *b0) { - const struct session *const *a = a0; - const struct session *const *b = b0; - const struct session *sa = *a; - const struct session *sb = *b; - int result = 0; + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; + const struct session *const *a = a0; + const struct session *const *b = b0; + const struct session *sa = *a; + const struct session *sb = *b; + int result = 0; - switch (format_loop_sort_criteria.field) { + switch (sc->field) { case FORMAT_LOOP_BY_INDEX: result = sa->id - sb->id; break; @@ -4264,7 +4384,7 @@ format_cmp_session(const void *a0, const void *b0) break; } - if (format_loop_sort_criteria.reversed) + if (sc->reversed) result = -result; return (result); } @@ -4281,7 +4401,7 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) char *all, *active, *use, *expanded, *value; size_t valuelen; struct session *s; - int i, n; + int i, n, last = 0; static struct session **l = NULL; static int lsz = 0; @@ -4311,7 +4431,9 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) use = active; else use = all; - nft = format_create(c, item, FORMAT_NONE, ft->flags); + if (i == n - 1) + last = FORMAT_LAST; + nft = format_create(c, item, FORMAT_NONE, ft->flags|last); format_defaults(nft, ft->c, s, NULL, NULL); format_copy_state(&next, es, 0); next.ft = nft; @@ -4352,6 +4474,40 @@ format_window_name(struct format_expand_state *es, const char *fmt) return (xstrdup("0")); } +static int +format_cmp_window(const void *a0, const void *b0) +{ + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; + const struct winlink *const *a = a0; + const struct winlink *const *b = b0; + const struct window *wa = (*a)->window; + const struct window *wb = (*b)->window; + int result = 0; + + switch (sc->field) { + case FORMAT_LOOP_BY_INDEX: + result = wa->id - wb->id; + break; + case FORMAT_LOOP_BY_TIME: + if (timercmp(&wa->activity_time, &wb->activity_time, >)) { + result = -1; + break; + } + if (timercmp(&wa->activity_time, &wb->activity_time, <)) { + result = 1; + break; + } + /* FALLTHROUGH */ + case FORMAT_LOOP_BY_NAME: + result = strcmp(wa->name, wb->name); + break; + } + + if (sc->reversed) + result = -result; + return (result); +} + /* Loop over windows. */ static char * format_loop_windows(struct format_expand_state *es, const char *fmt) @@ -4365,6 +4521,9 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) size_t valuelen; struct winlink *wl; struct window *w; + int i, n, last = 0; + static struct winlink **l = NULL; + static int lsz = 0; if (ft->s == NULL) { format_log(es, "window loop but no session"); @@ -4376,17 +4535,32 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) active = NULL; } + n = 0; + RB_FOREACH(wl, winlinks, &ft->s->windows) { + if (lsz <= n) { + lsz += 100; + l = xreallocarray(l, lsz, sizeof *l); + } + l[n++] = wl; + } + + qsort(l, n, sizeof *l, format_cmp_window); + value = xcalloc(1, 1); valuelen = 1; - RB_FOREACH(wl, winlinks, &ft->s->windows) { + for (i = 0; i < n; i++) { + wl = l[i]; w = wl->window; format_log(es, "window loop: %u @%u", wl->idx, w->id); if (active != NULL && wl == ft->s->curw) use = active; else use = all; - nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags); + if (i == n - 1) + last = FORMAT_LAST; + nft = format_create(c, item, FORMAT_WINDOW|w->id, + ft->flags|last); format_defaults(nft, ft->c, ft->s, wl, NULL); format_copy_state(&next, es, 0); next.ft = nft; @@ -4406,6 +4580,23 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) return (value); } +static int +format_cmp_pane(const void *a0, const void *b0) +{ + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; + const struct window_pane *const *a = a0; + const struct window_pane *const *b = b0; + const struct window_pane *wpa = *a; + const struct window_pane *wpb = *b; + int result = 0; + + if (sc->reversed) + result = wpb->id - wpa->id; + else + result = wpa->id - wpb->id; + return (result); +} + /* Loop over panes. */ static char * format_loop_panes(struct format_expand_state *es, const char *fmt) @@ -4418,6 +4609,9 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) char *all, *active, *use, *expanded, *value; size_t valuelen; struct window_pane *wp; + int i, n, last = 0; + static struct window_pane **l = NULL; + static int lsz = 0; if (ft->w == NULL) { format_log(es, "pane loop but no window"); @@ -4429,16 +4623,31 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) active = NULL; } + n = 0; + TAILQ_FOREACH(wp, &ft->w->panes, entry) { + if (lsz <= n) { + lsz += 100; + l = xreallocarray(l, lsz, sizeof *l); + } + l[n++] = wp; + } + + qsort(l, n, sizeof *l, format_cmp_pane); + value = xcalloc(1, 1); valuelen = 1; - TAILQ_FOREACH(wp, &ft->w->panes, entry) { + for (i = 0; i < n; i++) { + wp = l[i]; format_log(es, "pane loop: %%%u", wp->id); if (active != NULL && wp == ft->w->active) use = active; else use = all; - nft = format_create(c, item, FORMAT_PANE|wp->id, ft->flags); + if (i == n - 1) + last = FORMAT_LAST; + nft = format_create(c, item, FORMAT_PANE|wp->id, + ft->flags|last); format_defaults(nft, ft->c, ft->s, ft->wl, wp); format_copy_state(&next, es, 0); next.ft = nft; @@ -4458,24 +4667,86 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) return (value); } +static int +format_cmp_client(const void *a0, const void *b0) +{ + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; + const struct client *const *a = a0; + const struct client *const *b = b0; + const struct client *ca = *a; + const struct client *cb = *b; + int result = 0; + + switch (sc->field) { + case FORMAT_LOOP_BY_INDEX: + break; + case FORMAT_LOOP_BY_TIME: + if (timercmp(&ca->activity_time, &cb->activity_time, >)) { + result = -1; + break; + } + if (timercmp(&ca->activity_time, &cb->activity_time, <)) { + result = 1; + break; + } + /* FALLTHROUGH */ + case FORMAT_LOOP_BY_NAME: + result = strcmp(ca->name, cb->name); + break; + } + + if (sc->reversed) + result = -result; + return (result); +} + /* Loop over clients. */ static char * format_loop_clients(struct format_expand_state *es, const char *fmt) { - struct format_tree *ft = es->ft; - struct client *c; - struct cmdq_item *item = ft->item; - struct format_tree *nft; - struct format_expand_state next; - char *expanded, *value; - size_t valuelen; + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; + struct format_tree *ft = es->ft; + struct client *c; + struct cmdq_item *item = ft->item; + struct format_tree *nft; + struct format_expand_state next; + char *expanded, *value; + size_t valuelen; + int i, n, last = 0; + static struct client **l = NULL; + static int lsz = 0; value = xcalloc(1, 1); valuelen = 1; + n = 0; TAILQ_FOREACH(c, &clients, entry) { + if (lsz <= n) { + lsz += 100; + l = xreallocarray(l, lsz, sizeof *l); + } + l[n++] = c; + } + + if (sc->field != FORMAT_LOOP_BY_INDEX) + qsort(l, n, sizeof *l, format_cmp_client); + else { + /* Use order in the TAILQ as "index" order. */ + if (sc->reversed) { + for (i = 0; i < n / 2; i++) { + c = l[i]; + l[i] = l[n - 1 - i]; + l[n - 1 - i] = c; + } + } + } + + for (i = 0; i < n; i++) { + c = l[i]; format_log(es, "client loop: %s", c->name); - nft = format_create(c, item, 0, ft->flags); + if (i == n - 1) + last = FORMAT_LAST; + nft = format_create(c, item, 0, ft->flags|last); format_defaults(nft, c, ft->s, ft->wl, ft->wp); format_copy_state(&next, es, 0); next.ft = nft; @@ -4639,6 +4910,7 @@ static int format_replace(struct format_expand_state *es, const char *key, size_t keylen, char **buf, size_t *len, size_t *off) { + struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; struct format_tree *ft = es->ft; struct window_pane *wp = ft->wp; const char *errstr, *copy, *cp, *cp2; @@ -4654,7 +4926,6 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, struct format_modifier *bool_op_n = NULL; u_int i, count, nsub = 0, nrep; struct format_expand_state next; - struct format_loop_sort_criteria *sc = &format_loop_sort_criteria; /* Make a copy of the key. */ copy = copy0 = xstrndup(key, keylen); @@ -4765,15 +5036,19 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, break; case 'S': modifiers |= FORMAT_SESSIONS; - if (fm->argc < 1) + if (fm->argc < 1) { + sc->field = FORMAT_LOOP_BY_INDEX; + sc->reversed = 0; break; + } if (strchr(fm->argv[0], 'i') != NULL) sc->field = FORMAT_LOOP_BY_INDEX; else if (strchr(fm->argv[0], 'n') != NULL) sc->field = FORMAT_LOOP_BY_NAME; else if (strchr(fm->argv[0], 't') != NULL) sc->field = FORMAT_LOOP_BY_TIME; - else sc->field = FORMAT_LOOP_BY_INDEX; + else + sc->field = FORMAT_LOOP_BY_INDEX; if (strchr(fm->argv[0], 'r') != NULL) sc->reversed = 1; else @@ -4781,12 +5056,54 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, break; case 'W': modifiers |= FORMAT_WINDOWS; + if (fm->argc < 1) { + sc->field = FORMAT_LOOP_BY_INDEX; + sc->reversed = 0; + break; + } + if (strchr(fm->argv[0], 'i') != NULL) + sc->field = FORMAT_LOOP_BY_INDEX; + else if (strchr(fm->argv[0], 'n') != NULL) + sc->field = FORMAT_LOOP_BY_NAME; + else if (strchr(fm->argv[0], 't') != NULL) + sc->field = FORMAT_LOOP_BY_TIME; + else + sc->field = FORMAT_LOOP_BY_INDEX; + if (strchr(fm->argv[0], 'r') != NULL) + sc->reversed = 1; + else + sc->reversed = 0; break; case 'P': modifiers |= FORMAT_PANES; + if (fm->argc < 1) { + sc->reversed = 0; + break; + } + if (strchr(fm->argv[0], 'r') != NULL) + sc->reversed = 1; + else + sc->reversed = 0; break; case 'L': modifiers |= FORMAT_CLIENTS; + if (fm->argc < 1) { + sc->field = FORMAT_LOOP_BY_INDEX; + sc->reversed = 0; + break; + } + if (strchr(fm->argv[0], 'i') != NULL) + sc->field = FORMAT_LOOP_BY_INDEX; + else if (strchr(fm->argv[0], 'n') != NULL) + sc->field = FORMAT_LOOP_BY_NAME; + else if (strchr(fm->argv[0], 't') != NULL) + sc->field = FORMAT_LOOP_BY_TIME; + else + sc->field = FORMAT_LOOP_BY_INDEX; + if (strchr(fm->argv[0], 'r') != NULL) + sc->reversed = 1; + else + sc->reversed = 0; break; case 'R': modifiers |= FORMAT_REPEAT; diff --git a/options-table.c b/options-table.c index aca75332..0ee49675 100644 --- a/options-table.c +++ b/options-table.c @@ -139,7 +139,7 @@ static const char *options_table_allow_passthrough_list[] = { "#{T:window-status-format}" \ "#[pop-default]" \ "#[norange default]" \ - "#{?window_end_flag,,#{window-status-separator}}" \ + "#{?loop_last_flag,,#{window-status-separator}}" \ "," \ "#[range=window|#{window_index} list=focus " \ "#{?#{!=:#{E:window-status-current-style},default}," \ @@ -166,7 +166,7 @@ static const char *options_table_allow_passthrough_list[] = { "#{T:window-status-current-format}" \ "#[pop-default]" \ "#[norange list=on default]" \ - "#{?window_end_flag,,#{window-status-separator}}" \ + "#{?loop_last_flag,,#{window-status-separator}}" \ "}" \ "#[nolist align=right range=right #{E:status-right-style}]" \ "#[push-default]" \ diff --git a/tmux.1 b/tmux.1 index 1d0ec120..00d19381 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5909,14 +5909,21 @@ or .Ql L:\& will loop over each session, window, pane or client and insert the format once for each. -.Ql S:\& , +.Ql L:\& , +.Ql S:\& +and +.Ql W:\& can take an optional sort argument .Ql /i\& , .Ql /n\& , .Ql /t\& -to sort by index, name, or time; or +to sort by index, name, or last activity time; additionally .Ql /r\& to sort in reverse order. +.Ql /r\& +can also be used with +.Ql P:\& +to reverse the sort order by pane index. For example, .Ql S/nr:\& to sort sessions by name in reverse order. @@ -6068,8 +6075,10 @@ The following variables are available, where appropriate: .It Li "insert_flag" Ta "" Ta "Pane insert flag" .It Li "keypad_cursor_flag" Ta "" Ta "Pane keypad cursor flag" .It Li "keypad_flag" Ta "" Ta "Pane keypad flag" +.It Li "last_session_index" Ta "" Ta "Index of last session" .It Li "last_window_index" Ta "" Ta "Index of last window in session" .It Li "line" Ta "" Ta "Line number in the list" +.It Li "loop_last_flag" Ta "" Ta "1 if last window, pane, session, client in the W:, P:, S:, or L: loop" .It Li "mouse_all_flag" Ta "" Ta "Pane mouse all flag" .It Li "mouse_any_flag" Ta "" Ta "Pane mouse any flag" .It Li "mouse_button_flag" Ta "" Ta "Pane mouse button flag" @@ -6141,10 +6150,13 @@ The following variables are available, where appropriate: .It Li "selection_start_x" Ta "" Ta "X position of the start of the selection" .It Li "selection_start_y" Ta "" Ta "Y position of the start of the selection" .It Li "server_sessions" Ta "" Ta "Number of sessions" +.It Li "session_active" Ta "" Ta "1 if session active" .It Li "session_activity" Ta "" Ta "Time of session last activity" +.It Li "session_activity_flag" Ta "" Ta "1 if any window in session has activity" .It Li "session_alerts" Ta "" Ta "List of window indexes with alerts" .It Li "session_attached" Ta "" Ta "Number of clients session is attached to" .It Li "session_attached_list" Ta "" Ta "List of clients session is attached to" +.It Li "session_bell_flag" Ta "" Ta "1 if any window in session has bell" .It Li "session_created" Ta "" Ta "Time session created" .It Li "session_format" Ta "" Ta "1 if format is for a session" .It Li "session_group" Ta "" Ta "Name of session group" @@ -6155,11 +6167,13 @@ The following variables are available, where appropriate: .It Li "session_group_size" Ta "" Ta "Size of session group" .It Li "session_grouped" Ta "" Ta "1 if session in a group" .It Li "session_id" Ta "" Ta "Unique session ID" +.It Li "session_index" Ta "" Ta "Index of session" .It Li "session_last_attached" Ta "" Ta "Time session last attached" .It Li "session_many_attached" Ta "" Ta "1 if multiple clients attached" .It Li "session_marked" Ta "" Ta "1 if this session contains the marked pane" .It Li "session_name" Ta "#S" Ta "Name of session" .It Li "session_path" Ta "" Ta "Working directory of session" +.It Li "session_silence_flag" Ta "" Ta "1 if any window in session has silence alert" .It Li "session_stack" Ta "" Ta "Window indexes in most recent order" .It Li "session_windows" Ta "" Ta "Number of windows in session" .It Li "socket_path" Ta "" Ta "Server socket path" diff --git a/tmux.h b/tmux.h index 61de21ae..f3d0252e 100644 --- a/tmux.h +++ b/tmux.h @@ -2256,6 +2256,7 @@ char *paste_make_sample(struct paste_buffer *); #define FORMAT_FORCE 0x2 #define FORMAT_NOJOBS 0x4 #define FORMAT_VERBOSE 0x8 +#define FORMAT_LAST 0x10 #define FORMAT_NONE 0 #define FORMAT_PANE 0x80000000U #define FORMAT_WINDOW 0x40000000U