From 457b6cbefd91361e2c973feae93dd0f5c7c21106 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 20 Jul 2026 11:24:33 +0100 Subject: [PATCH 1/8] Put OSC 133 data into grid_line. --- cmd-capture-pane.c | 18 +++++-- grid.c | 29 ++++++++++- input.c | 111 ++++++++++++++++++++++++++++++----------- regress/input-modes.sh | 19 +++++-- screen-write.c | 5 +- tmux.h | 36 ++++++++++--- window-copy.c | 4 +- 7 files changed, 174 insertions(+), 48 deletions(-) diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index a17ae6243..81abda888 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -134,6 +134,7 @@ cmd_capture_pane_grid(struct window_pane *wp, size_t *len) struct screen *s = &wp->base; struct grid *gd = s->grid; struct grid_line *gl; + struct osc133_data *od; char *buf = xstrdup(""), *line; char p[11]; u_int yy, xx, total = gd->hsize + gd->sy; @@ -149,9 +150,20 @@ cmd_capture_pane_grid(struct window_pane *wp, size_t *len) snprintf(p, sizeof p, "-"); else snprintf(p, sizeof p, "%u", yy - gd->hsize); - xasprintf(&line, "\tL %u (%s) flags=%s[%x] %u/%u\n", yy, - p, grid_line_flags_string(gl->flags), gl->flags, - gl->cellused, gl->cellsize); + od = &gl->osc133_data; + if (gl->flags & GRID_LINE_OSC133_FLAGS) { + xasprintf(&line, "\tL %u (%s) flags=%s[%x] " + "%u/%u osc133=%u,%u,%u,%u,%u\n", yy, p, + grid_line_flags_string(gl->flags), gl->flags, + gl->cellused, gl->cellsize, od->prompt_col, + od->cmd_col, od->out_start_col, + od->out_end_col, od->exit_status); + } else { + xasprintf(&line, "\tL %u (%s) flags=%s[%x] " + "%u/%u\n", yy, p, + grid_line_flags_string(gl->flags), gl->flags, + gl->cellused, gl->cellsize); + } buf = cmd_capture_pane_append(buf, len, line, strlen(line)); free(line); diff --git a/grid.c b/grid.c index d753e03f1..0295bcf12 100644 --- a/grid.c +++ b/grid.c @@ -233,6 +233,25 @@ grid_get_line(struct grid *gd, u_int line) return (&gd->linedata[line]); } +/* Get line time. */ +time_t +grid_line_time(const struct grid_line *gl) +{ + if (gl->time == 0) + return (0); + return (start_time.tv_sec + gl->time - 1); +} + +/* Set line time. */ +static void +grid_line_set_time(struct grid_line *gl) +{ + if (current_time == 0) + gl->time = 0; + else + gl->time = current_time - start_time.tv_sec + 1; +} + /* Adjust number of lines. */ void grid_adjust_lines(struct grid *gd, u_int lines) @@ -486,7 +505,7 @@ grid_scroll_history(struct grid *gd, u_int bg) gd->hscrolled++; grid_compact_line(&gd->linedata[gd->hsize]); - gd->linedata[gd->hsize].time = current_time; + grid_line_set_time(&gd->linedata[gd->hsize]); gd->hsize++; gd->scroll_added++; } @@ -528,7 +547,7 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) /* Move the line into the history. */ memcpy(gl_history, gl_upper, sizeof *gl_history); - gl_history->time = current_time; + grid_line_set_time(gl_history); /* Then move the region up and clear the bottom line. */ memmove(gl_upper, gl_upper + 1, (lower - upper) * sizeof *gl_upper); @@ -1701,8 +1720,14 @@ grid_line_flags_string(int flags) strlcat(s, "DEAD,", sizeof s); if (flags & GRID_LINE_START_PROMPT) strlcat(s, "START_PROMPT,", sizeof s); + if (flags & GRID_LINE_SECOND_PROMPT) + strlcat(s, "SECOND_PROMPT,", sizeof s); + if (flags & GRID_LINE_START_COMMAND) + strlcat(s, "START_COMMAND,", sizeof s); if (flags & GRID_LINE_START_OUTPUT) strlcat(s, "START_OUTPUT,", sizeof s); + if (flags & GRID_LINE_END_OUTPUT) + strlcat(s, "END_OUTPUT,", sizeof s); if (flags & GRID_LINE_HYPERLINK) strlcat(s, "HYPERLINK,", sizeof s); if (*s == '\0') diff --git a/input.c b/input.c index 0aac9bfbb..09c26fe22 100644 --- a/input.c +++ b/input.c @@ -174,8 +174,6 @@ static void input_osc_110(struct input_ctx *, const char *); static void input_osc_111(struct input_ctx *, const char *); static void input_osc_112(struct input_ctx *, const char *); static void input_osc_133(struct input_ctx *, const char *); -static void input_fire_pane_title_changed(struct window_pane *, - const char *); /* Transition entry/exit handlers. */ static void input_clear(struct input_ctx *); @@ -212,21 +210,6 @@ static int input_end_bel(struct input_ctx *); /* Command table comparison function. */ static int input_table_compare(const void *, const void *); -static void -input_fire_pane_title_changed(struct window_pane *wp, const char *title) -{ - struct event_payload *ep; - struct cmd_find_state fs; - - ep = event_payload_create(); - cmd_find_from_pane(&fs, wp, 0); - event_payload_set_target(ep, &fs); - event_payload_set_pane(ep, "pane", wp); - event_payload_set_window(ep, "window", wp->window); - event_payload_set_string(ep, "new_title", "%s", title); - events_fire("pane-title-changed", ep); -} - /* Command table entry. */ struct input_table_entry { int ch; @@ -809,6 +792,22 @@ input_stop_utf8(struct input_ctx *ictx) ictx->utf8started = 0; } +/* Fire a title changed event. */ +static void +input_fire_pane_title_changed(struct window_pane *wp, const char *title) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); +} + /* * Timer - if this expires then have been waiting for a terminator for too * long, so reset to ground. @@ -3192,6 +3191,35 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Parse the OSC 133 D exit status. */ +static int +input_osc_133_exit_status(const char *p) +{ + const char *end; + char *copy; + const char *errstr; + long long status; + + if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2) + return (0); + end = strchr(p + 2, ';'); + if (end == p + 2) + return (0); + if (end == NULL) + copy = xstrdup(p + 2); + else + copy = xstrndup(p + 2, end - (p + 2)); + if (strchr(copy, '=') != NULL) { + free(copy); + return (0); + } + status = strtonum(copy, 0, 255, &errstr); + free(copy); + if (errstr != NULL) + return (255); + return (status); +} + /* Fire an OSC 133 command event. */ static void input_fire_command_event(struct window_pane *wp, const char *name) @@ -3237,27 +3265,51 @@ static void input_osc_133(struct input_ctx *ictx, const char *p) { struct window_pane *wp = ictx->wp; - struct grid *gd = ictx->ctx.s->grid; - u_int line = ictx->ctx.s->cy + gd->hsize; + struct screen *s = ictx->ctx.s; + struct grid *gd = s->grid; + u_int line = s->cy + gd->hsize; struct grid_line *gl = NULL; - const char *errstr; + const char *cp; int status; - if (line <= gd->hsize + gd->sy - 1) + if (line < gd->hsize + gd->sy) gl = grid_get_line(gd, line); switch (*p) { case 'A': - if (gl != NULL) + case 'N': + if (gl != NULL) { + memset(&gl->osc133_data, 0, sizeof gl->osc133_data); + gl->osc133_data.prompt_col = s->cx; gl->flags |= GRID_LINE_START_PROMPT; + } if (wp != NULL) { wp->last_prompt_time = time(NULL); events_fire_pane("pane-shell-prompt", wp); } break; + case 'P': + if (gl != NULL) { + cp = strstr(p, ";k=s"); + if (cp != NULL && (cp[4] == ';' || cp[4] == '\0')) + gl->flags |= GRID_LINE_SECOND_PROMPT; + else + gl->flags |= GRID_LINE_START_PROMPT; + gl->osc133_data.prompt_col = s->cx; + } + break; + case 'B': + case 'I': + if (gl != NULL) { + gl->flags |= GRID_LINE_START_COMMAND; + gl->osc133_data.cmd_col = s->cx; + } + break; case 'C': - if (gl != NULL) + if (gl != NULL) { gl->flags |= GRID_LINE_START_OUTPUT; + gl->osc133_data.out_start_col = s->cx; + } if (wp != NULL) { wp->cmd_start_time = time(NULL); wp->cmd_end_time = 0; @@ -3267,17 +3319,18 @@ input_osc_133(struct input_ctx *ictx, const char *p) } break; case 'D': + status = input_osc_133_exit_status(p); if (wp != NULL) { wp->cmd_end_time = time(NULL); wp->flags &= ~PANE_CMDRUNNING; - wp->cmd_status = -1; - if (p[1] == ';' && p[2] != '\0') { - status = strtonum(p + 2, 0, INT_MAX, &errstr); - if (errstr == NULL) - wp->cmd_status = status; - } + wp->cmd_status = status; input_fire_command_event(wp, "pane-command-finished"); } + if (gl != NULL) { + gl->flags |= GRID_LINE_END_OUTPUT; + gl->osc133_data.out_end_col = s->cx; + gl->osc133_data.exit_status = status; + } break; } } diff --git a/regress/input-modes.sh b/regress/input-modes.sh index 9bbe44429..8ae905f89 100644 --- a/regress/input-modes.sh +++ b/regress/input-modes.sh @@ -5,11 +5,20 @@ start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n' check_capture alternate 'MAINZ' -start_pane osc133 10 4 '\033]133;A\007prompt\n\033]133;C\007output\n' -check_capture osc133 'prompt -output' -check_flags osc133 'P prompt -O output' +start_pane osc133 20 8 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n' +check_capture osc133 'xxp>cmd +xymore +zzout +qbad +qqbig +zzzok' +check_raw_matches osc133 \ + 'L 0 \(0\) flags=START_PROMPT,START_COMMAND\[[0-9a-f]+\].* osc133=2,4,0,0,0' \ + 'L 1 \(1\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \ + 'L 2 \(2\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,7' \ + 'L 3 \(3\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,1,4,255' \ + 'L 4 \(4\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,255' \ + 'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0' $TMUX kill-server 2>/dev/null exit $exit_status diff --git a/screen-write.c b/screen-write.c index a311296e7..d9654f238 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1606,6 +1606,7 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) struct grid_line *gl; u_int sx = screen_size_x(s); struct screen_write_citem *ci = ctx->item; + struct osc133_data od; u_int flags; gl = grid_get_line(s->grid, s->grid->hsize + s->cy); @@ -1617,10 +1618,12 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg) ctx->wp->flags |= PANE_REDRAW; #endif - flags = gl->flags & (GRID_LINE_START_PROMPT|GRID_LINE_START_OUTPUT); + flags = gl->flags & GRID_LINE_OSC133_FLAGS; + memcpy(&od, &gl->osc133_data, sizeof od); grid_view_clear(s->grid, 0, s->cy, sx, 1, bg); gl = grid_get_line(s->grid, s->grid->hsize + s->cy); gl->flags |= flags; + memcpy(&gl->osc133_data, &od, sizeof gl->osc133_data); screen_write_collect_clear(ctx, s->cy, 1); ci->x = 0; diff --git a/tmux.h b/tmux.h index c504b4757..f030610eb 100644 --- a/tmux.h +++ b/tmux.h @@ -809,8 +809,19 @@ struct colour_palette { #define GRID_LINE_EXTENDED 0x2 #define GRID_LINE_DEAD 0x4 #define GRID_LINE_START_PROMPT 0x8 -#define GRID_LINE_START_OUTPUT 0x10 -#define GRID_LINE_HYPERLINK 0x20 +#define GRID_LINE_SECOND_PROMPT 0x10 +#define GRID_LINE_START_COMMAND 0x20 +#define GRID_LINE_START_OUTPUT 0x40 +#define GRID_LINE_END_OUTPUT 0x80 +#define GRID_LINE_HYPERLINK 0x100 + +/* All OSC 133 flags. */ +#define GRID_LINE_OSC133_FLAGS \ + (GRID_LINE_START_PROMPT| \ + GRID_LINE_SECOND_PROMPT| \ + GRID_LINE_START_COMMAND| \ + GRID_LINE_START_OUTPUT| \ + GRID_LINE_END_OUTPUT) /* Grid string flags. */ #define GRID_STRING_WITH_SEQUENCES 0x1 @@ -879,17 +890,27 @@ struct grid_cell_entry { u_char flags; } __packed; +/* OSC 133 data for a grid line. */ +struct osc133_data { + u_short prompt_col; + u_short cmd_col; + u_short out_start_col; + u_short out_end_col; + u_char exit_status; +}; + /* Grid line. */ struct grid_line { struct grid_cell_entry *celldata; - u_int cellused; - u_int cellsize; - struct grid_extd_entry *extddata; + + u_short cellused; + u_short cellsize; u_int extdsize; - int flags; - time_t time; + u_int time; + struct osc133_data osc133_data; + u_short flags; }; /* Entire grid of cells. */ @@ -3451,6 +3472,7 @@ int grid_compare(struct grid *, struct grid *); const char *grid_line_flags_string(int); const char *grid_cell_flags_string(int); const char *grid_cell_attr_string(int); +time_t grid_line_time(const struct grid_line *); void grid_collect_history(struct grid *, int); void grid_remove_history(struct grid *, u_int ); void grid_scroll_history(struct grid *, u_int); diff --git a/window-copy.c b/window-copy.c index a6909a8d4..c2b2542e7 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1107,9 +1107,11 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) u_int hsize = screen_hsize(data->backing); u_int position, limit; struct grid_line *gl; + time_t t; gl = grid_get_line(data->backing->grid, hsize - data->oy); - format_add(ft, "top_line_time", "%llu", (unsigned long long)gl->time); + t = grid_line_time(gl); + format_add(ft, "top_line_time", "%llu", (unsigned long long)t); format_add(ft, "scroll_position", "%d", data->oy); if (window_copy_line_number_is_absolute(wme)) { From f41288ee27f545daf423444969a152400260c9c2 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Mon, 20 Jul 2026 16:26:02 +0100 Subject: [PATCH 2/8] Add collapsible lines. --- cmd-copy-mode.c | 4 +- input.c | 28 +- key-bindings.c | 8 +- options-table.c | 11 + tmux.1 | 47 ++- tty-acs.c | 2 + window-copy.c | 792 ++++++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 851 insertions(+), 41 deletions(-) diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index e6888ca7e..94c19da68 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = { .name = "copy-mode", .alias = NULL, - .args = { "dekHMqSs:t:u", 0, 0, NULL }, - .usage = "[-dekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, + .args = { "UcdekHMqSs:t:u", 0, 0, NULL }, + .usage = "[-UcdekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, .source = { 's', CMD_FIND_PANE, 0 }, .target = { 't', CMD_FIND_PANE, 0 }, diff --git a/input.c b/input.c index 09c26fe22..2f158ca4d 100644 --- a/input.c +++ b/input.c @@ -3279,9 +3279,14 @@ input_osc_133(struct input_ctx *ictx, const char *p) case 'A': case 'N': if (gl != NULL) { - memset(&gl->osc133_data, 0, sizeof gl->osc133_data); - gl->osc133_data.prompt_col = s->cx; - gl->flags |= GRID_LINE_START_PROMPT; + if (!(gl->flags & (GRID_LINE_START_PROMPT| + GRID_LINE_SECOND_PROMPT))) { + gl->osc133_data.prompt_col = s->cx; + if (strstr(p + 1, ";k=s") != NULL) + gl->flags |= GRID_LINE_SECOND_PROMPT; + else + gl->flags |= GRID_LINE_START_PROMPT; + } } if (wp != NULL) { wp->last_prompt_time = time(NULL); @@ -3291,22 +3296,25 @@ input_osc_133(struct input_ctx *ictx, const char *p) case 'P': if (gl != NULL) { cp = strstr(p, ";k=s"); - if (cp != NULL && (cp[4] == ';' || cp[4] == '\0')) - gl->flags |= GRID_LINE_SECOND_PROMPT; - else - gl->flags |= GRID_LINE_START_PROMPT; - gl->osc133_data.prompt_col = s->cx; + if (!(gl->flags & (GRID_LINE_START_PROMPT| + GRID_LINE_SECOND_PROMPT))) { + gl->osc133_data.prompt_col = s->cx; + if (cp != NULL && (cp[4] == ';' || cp[4] == '\0')) + gl->flags |= GRID_LINE_SECOND_PROMPT; + else + gl->flags |= GRID_LINE_START_PROMPT; + } } break; case 'B': case 'I': - if (gl != NULL) { + if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) { gl->flags |= GRID_LINE_START_COMMAND; gl->osc133_data.cmd_col = s->cx; } break; case 'C': - if (gl != NULL) { + if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) { gl->flags |= GRID_LINE_START_OUTPUT; gl->osc133_data.out_start_col = s->cx; } diff --git a/key-bindings.c b/key-bindings.c index ff6111870..42b6cd773 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -577,6 +577,8 @@ key_bindings_init(void) "bind -Tcopy-mode Escape { send -X cancel }", "bind -Tcopy-mode C-[ { send -X cancel }", "bind -Tcopy-mode Space { send -X page-down }", + "bind -Tcopy-mode Tab { send -X toggle-output }", +"bind -Tcopy-mode BTab { send -X toggle-output -a }", "bind -Tcopy-mode , { send -X jump-reverse }", "bind -Tcopy-mode \\; { send -X jump-again }", "bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }", @@ -594,7 +596,7 @@ key_bindings_init(void) "bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode Home { send -X start-of-line }", "bind -Tcopy-mode End { send -X end-of-line }", - "bind -Tcopy-mode MouseDown1Pane select-pane", + "bind -Tcopy-mode MouseDown1Pane { select-pane; send -X toggle-output -m }", "bind -Tcopy-mode MouseDrag1Pane { select-pane; send -X begin-selection }", "bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }", "bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }", @@ -654,6 +656,8 @@ key_bindings_init(void) "bind -Tcopy-mode-vi Escape { send -X clear-selection }", "bind -Tcopy-mode-vi C-[ { send -X clear-selection }", "bind -Tcopy-mode-vi Space { send -X begin-selection }", + "bind -Tcopy-mode-vi Tab { send -X toggle-output }", +"bind -Tcopy-mode-vi BTab { send -X toggle-output -a }", "bind -Tcopy-mode-vi '$' { send -X end-of-line }", "bind -Tcopy-mode-vi , { send -X jump-reverse }", "bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }", @@ -709,7 +713,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi % { send -X next-matching-bracket }", "bind -Tcopy-mode-vi Home { send -X start-of-line }", "bind -Tcopy-mode-vi End { send -X end-of-line }", - "bind -Tcopy-mode-vi MouseDown1Pane { select-pane }", + "bind -Tcopy-mode-vi MouseDown1Pane { select-pane; send -X toggle-output -m }", "bind -Tcopy-mode-vi MouseDrag1Pane { select-pane; send -X begin-selection }", "bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }", "bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }", diff --git a/options-table.c b/options-table.c index 4ab041ac6..35dada347 100644 --- a/options-table.c +++ b/options-table.c @@ -114,6 +114,9 @@ static const char *options_table_theme_list[] = { static const char *options_table_copy_mode_line_numbers_list[] = { "off", "default", "absolute", "relative", "hybrid", NULL }; +static const char *options_table_copy_mode_collapse_controls_list[] = { + "always", "never", "on-demand", NULL +}; /* Status line format. */ #define OPTIONS_TABLE_STATUS_FORMAT1 \ @@ -1373,6 +1376,14 @@ const struct options_table_entry options_table[] = { .text = "Style of search matches in copy mode." }, + { .name = "copy-mode-collapse-controls", + .type = OPTIONS_TABLE_CHOICE, + .scope = OPTIONS_TABLE_WINDOW, + .choices = options_table_copy_mode_collapse_controls_list, + .default_num = 2, + .text = "When OSC 133 output collapse controls are shown in copy mode." + }, + { .name = "copy-mode-current-match-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/tmux.1 b/tmux.1 index f40256350..7bc589737 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2167,6 +2167,27 @@ Hide line numbers in copy mode. .Xc Toggle line numbers in copy mode. .It Xo +.Ic collapse\-output +.Op Fl a +.Xc +Collapse the output for the command on the cursor line. +With +.Fl a , +collapse all command output in the copy-mode buffer. +Text before the first OSC 133 prompt is treated as an initial command output +region. +.It Xo +.Ic expand\-output +.Op Fl a +.Xc +Expand the output for the command on the cursor line. +With +.Fl a , +expand all command output in the copy-mode buffer. +.Pp +The Tab key toggles output for the command on the cursor line and S-Tab +toggles all command output. +.It Xo .Ic middle\-line (vi: M) (emacs: M\-r) @@ -2595,12 +2616,19 @@ The synopsis for the command is: .Bl -tag -width Ds .It Xo Ic copy\-mode -.Op Fl dekHMqSu +.Op Fl UcdekHMqSu .Op Fl s Ar src\-pane .Op Fl t Ar target\-pane .Xc Enter copy mode. .Pp +.Fl c +enters copy mode with OSC 133 command output collapsed. +.Fl U +enters copy mode with OSC 133 output controls shown and all output expanded. +When output is collapsed, the prompt and entered command are shown and text +from the output start marker to the next primary prompt marker is hidden. +.Pp .Fl u enters copy mode and scrolls one page up and .Fl d @@ -5811,6 +5839,23 @@ and .Ic hybrid , they use absolute line numbers. .Pp +.It Xo Ic copy\-mode\-collapse\-controls +.Op Ic always | never | on\-demand +.Xc +Choose when OSC 133 output collapse controls are shown in copy mode. +.Ic always +shows the control column in every copy-mode view; +.Ic never +does not show it, although collapse key bindings and commands still work; and +.Ic on\-demand +shows it after entering copy mode with +.Fl U +or +.Fl c , +or after output is changed with a copy-mode command or key binding. +The default is +.Ic on\-demand . +.Pp .It Xo Ic mode\-keys .Op Ic vi | emacs .Xc diff --git a/tty-acs.c b/tty-acs.c index eedb79c2f..62a7148f2 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -34,6 +34,8 @@ static const struct tty_acs_entry tty_acs_table[] = { { '-', "\342\206\221" }, /* arrow pointing up */ { '.', "\342\206\223" }, /* arrow pointing down */ { '0', "\342\226\256" }, /* solid square block */ + { '>', "\342\206\222" }, /* arrow pointing right */ + { 'V', "\342\206\223" }, /* arrow pointing down */ { '`', "\342\227\206" }, /* diamond */ { 'a', "\342\226\222" }, /* checker board (stipple) */ { 'b', "\342\220\211" }, diff --git a/window-copy.c b/window-copy.c index c2b2542e7..711299daa 100644 --- a/window-copy.c +++ b/window-copy.c @@ -28,6 +28,14 @@ struct window_copy_mode_data; +#define WINDOW_COPY_CONTROLS_ALWAYS 0 +#define WINDOW_COPY_CONTROLS_NEVER 1 +#define WINDOW_COPY_CONTROLS_ON_DEMAND 2 + +#define WINDOW_COPY_LINE_CONTROL 0x1 +#define WINDOW_COPY_LINE_MEMBER 0x2 +#define WINDOW_COPY_LINE_INITIAL 0x4 + static const char *window_copy_key_table(struct window_mode_entry *); static void window_copy_command(struct window_mode_entry *, struct client *, struct session *, struct winlink *, struct args *, @@ -63,6 +71,8 @@ static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); static u_int window_copy_line_number_width(struct window_mode_entry *); +static u_int window_copy_left_margin(struct window_mode_entry *); +static int window_copy_controls_visible(struct window_mode_entry *); static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, u_int); static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, @@ -71,6 +81,16 @@ static void window_copy_write_line(struct window_mode_entry *, struct screen_write_ctx *, u_int); static void window_copy_write_lines(struct window_mode_entry *, struct screen_write_ctx *, u_int, u_int); +static void window_copy_rebuild_backing(struct window_mode_entry *); +static int window_copy_set_output(struct window_mode_entry *, int, int, + u_int, int); +static int window_copy_find_output(struct window_mode_entry *, u_int, + u_int *, u_int *); +static int window_copy_restore_cursor(struct window_mode_entry *, u_int, + u_int); +static int window_copy_restore_command_end(struct window_mode_entry *, + u_int, u_int); +static int window_copy_restore_before(struct window_mode_entry *, u_int); static char *window_copy_match_at_cursor(struct window_copy_mode_data *); static void window_copy_scroll_to(struct window_mode_entry *, u_int, u_int, int); @@ -248,6 +268,12 @@ struct window_copy_cmd_state { struct winlink *wl; }; +struct window_copy_line { + u_int source_line; + u_int output_line; + u_char flags; +}; + /* * Copy mode's visible screen (the "screen" field) is filled from one of two * sources: the original contents of the pane (used when we actually enter via @@ -266,6 +292,11 @@ struct window_copy_mode_data { struct screen screen; struct screen *backing; + struct screen *source; + u_char *outputs; + u_int output_count; + struct window_copy_line *lines; + u_int line_count; int backing_written; /* backing display started */ struct input_ctx *ictx; @@ -299,6 +330,9 @@ struct window_copy_mode_data { int scroll_exit; /* exit on scroll to end? */ int hide_position; /* hide position marker */ int line_numbers; /* 0 off, 1 from option, 2 default */ + int output_gutter; + int output_controls; + int top_output; enum { SEL_CHAR, /* select one char at a time */ @@ -446,6 +480,278 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx, return (dst); } +static struct window_copy_line * +window_copy_get_line_info(struct window_copy_mode_data *data, u_int y) +{ + if (data->lines == NULL || y >= data->line_count) + return (NULL); + return (&data->lines[y]); +} + +static void +window_copy_set_line(struct window_copy_mode_data *data, u_int y, + u_int source_line, u_int output_line, u_char flags) +{ + struct window_copy_line *line; + + line = window_copy_get_line_info(data, y); + if (line == NULL) + return; + line->source_line = source_line; + line->output_line = output_line; + line->flags |= flags; +} + +static void +window_copy_set_control(struct window_copy_mode_data *data, u_int y, + u_int output_line) +{ + struct window_copy_line *line; + + line = window_copy_get_line_info(data, y); + if (line == NULL) + return; + line->output_line = output_line; + line->flags |= WINDOW_COPY_LINE_CONTROL; +} + +static int +window_copy_any_output_collapsed(struct window_copy_mode_data *data) +{ + struct grid *gd = data->source->grid; + struct grid_line *gl; + u_int y; + int command = 0, output = 0; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) + break; + } + if (y != 0 && y != gd->hsize + gd->sy && data->top_output) + return (1); + + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) { + command = 0; + output = 0; + } + if (gl->flags & GRID_LINE_START_COMMAND) + command = 1; + if (gl->flags & GRID_LINE_START_OUTPUT) { + if (data->outputs[y]) + return (1); + output = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (!output && command && data->outputs[y]) + return (1); + command = output = 0; + } + } + return (0); +} + +static int +window_copy_set_all_output(struct window_copy_mode_data *data, int collapse) +{ + struct grid *gd = data->source->grid; + struct grid_line *gl; + u_int y; + int changed = 0, command = 0, output = 0; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) + break; + } + if (y != 0 && y != gd->hsize + gd->sy && + data->top_output != collapse) { + data->top_output = collapse; + changed = 1; + } + + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) { + command = 0; + output = 0; + } + if (gl->flags & GRID_LINE_START_COMMAND) + command = 1; + if (gl->flags & GRID_LINE_START_OUTPUT) { + if (data->outputs[y] != collapse) { + data->outputs[y] = collapse; + changed = 1; + } + output = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (!output && command && data->outputs[y] != collapse) { + data->outputs[y] = collapse; + changed = 1; + } + command = output = 0; + } + } + return (changed); +} + +static int +window_copy_controls_visible(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + int controls; + + controls = options_get_number(wme->wp->window->options, + "copy-mode-collapse-controls"); + return (controls == WINDOW_COPY_CONTROLS_ALWAYS || + (controls == WINDOW_COPY_CONTROLS_ON_DEMAND && + data->output_controls)); +} + +/* Recreate the backing screen with any collapsed OSC 133 output omitted. */ +static void +window_copy_rebuild_backing(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + struct screen *src = data->source, *dst; + struct grid *sgd; + struct grid_line *sgl, *dgl; + struct screen_write_ctx ctx; + struct grid_cell gc; + struct osc133_data *osc133; + u_int y, x, total, dy, prompt_dy = UINT_MAX; + u_int output_line = UINT_MAX; + int collapsed = 0, command = 0, hiding = 0; + int initial = 0, was_hiding; + + if (src == NULL) + return; + if (!data->output_controls) { + dst = window_copy_clone_screen(src, &data->screen, NULL, NULL, 0); + if (data->backing != NULL) { + screen_free(data->backing); + free(data->backing); + } + free(data->lines); + data->lines = NULL; + data->line_count = 0; + data->backing = dst; + data->output_gutter = 0; + return; + } + sgd = src->grid; + total = sgd->hsize + sgd->sy; + dst = xcalloc(1, sizeof *dst); + screen_init(dst, screen_size_x(&data->screen), screen_size_y(&data->screen), + screen_hlimit(src)); + dst->grid->flags |= GRID_HISTORY; + free(data->lines); + data->lines = xcalloc(total, sizeof *data->lines); + data->line_count = total; + for (y = 0; y < total; y++) + data->lines[y].output_line = UINT_MAX; + for (y = 0; y < total; y++) { + sgl = grid_get_line(sgd, y); + if (sgl->flags & GRID_LINE_START_PROMPT) + break; + } + if (y != 0 && y != total) + initial = 1; + + screen_write_start(&ctx, dst); + for (y = 0; y < total; y++) { + sgl = grid_get_line(sgd, y); + osc133 = &sgl->osc133_data; + for (x = 0; x <= sgl->cellused; x++) { + dy = dst->grid->hsize + dst->cy; + was_hiding = hiding; + if (!hiding) + window_copy_set_line(data, dy, y, output_line, + output_line == UINT_MAX ? 0 : + WINDOW_COPY_LINE_MEMBER); + dgl = grid_get_line(dst->grid, dy); + if (initial && y == 0 && x == 0) + window_copy_set_line(data, dy, y, UINT_MAX, + WINDOW_COPY_LINE_CONTROL | WINDOW_COPY_LINE_INITIAL); + if ((sgl->flags & GRID_LINE_START_PROMPT) && + x == osc133->prompt_col) { + dgl->flags |= GRID_LINE_START_PROMPT; + dgl->osc133_data.prompt_col = dst->cx; + prompt_dy = dy; + output_line = UINT_MAX; + collapsed = command = hiding = 0; + } + if ((sgl->flags & GRID_LINE_SECOND_PROMPT) && + x == osc133->prompt_col) { + dgl->flags |= GRID_LINE_SECOND_PROMPT; + dgl->osc133_data.prompt_col = dst->cx; + } + if ((sgl->flags & GRID_LINE_START_COMMAND) && + x == osc133->cmd_col) { + dgl->flags |= GRID_LINE_START_COMMAND; + dgl->osc133_data.cmd_col = dst->cx; + command = 1; + } + if ((sgl->flags & GRID_LINE_START_OUTPUT) && + x == osc133->out_start_col) { + dgl->flags |= GRID_LINE_START_OUTPUT; + dgl->osc133_data.out_start_col = dst->cx; + output_line = y; + collapsed = data->outputs[y]; + hiding = collapsed; + if (prompt_dy != UINT_MAX) + window_copy_set_control(data, prompt_dy, output_line); + } + if ((sgl->flags & GRID_LINE_END_OUTPUT) && + x == osc133->out_end_col) { + dgl->flags |= GRID_LINE_END_OUTPUT; + dgl->osc133_data.out_end_col = dst->cx; + dgl->osc133_data.exit_status = osc133->exit_status; + if (output_line == UINT_MAX && command) { + output_line = y; + collapsed = data->outputs[y]; + if (prompt_dy != UINT_MAX) + window_copy_set_control(data, prompt_dy, + output_line); + } + if (initial && prompt_dy == UINT_MAX) + hiding = data->top_output; + else + hiding = collapsed; + command = 0; + } + if (was_hiding && !hiding) + window_copy_set_line(data, dy, y, output_line, + output_line == UINT_MAX ? 0 : + WINDOW_COPY_LINE_MEMBER); + if (x == sgl->cellused) + break; + if (hiding) + continue; + grid_get_cell(sgd, x, y, &gc); + if (!(gc.flags & GRID_FLAG_PADDING)) + screen_write_cell(&ctx, &gc); + } + if (y + 1 != total && !(sgl->flags & GRID_LINE_WRAPPED) && + (!hiding || (initial && y == 0))) { + screen_write_carriagereturn(&ctx); + screen_write_linefeed(&ctx, 0, 8); + } + if (initial && y == 0) + hiding = data->top_output; + } + screen_write_stop(&ctx); + if (data->backing != NULL) { + screen_free(data->backing); + free(data->backing); + } + data->backing = dst; + data->output_gutter = window_copy_controls_visible(wme); +} + /* * Snapshot the source grid's monotonic scroll counters so the next incremental * sync can tell how much history was added or collected since this point. @@ -473,20 +779,20 @@ window_copy_sync_backing(struct window_mode_entry *wme) struct window_copy_mode_data *data = wme->data; struct window_pane *wp = wme->swp; struct screen *src = &wp->base; - struct screen *dst = data->backing; + struct screen *dst = data->source; struct grid *sg = src->grid; struct grid *dg = dst->grid; u_int sy = sg->sy; u_int old_hsize = dg->hsize; u_int new_hsize = sg->hsize; - u_int added, collected, kept; + u_int added, collected, kept, old_count, new_count; /* * Only a pane's own live grid is tracked incrementally. A different * source pane (copy-mode -s) goes through clone_screen, which also * trims trailing blank lines that this path does not. */ - if (data->viewmode || wme->swp != wme->wp) + if (data->source == NULL || data->viewmode || wme->swp != wme->wp) return (0); /* Indices only line up at the same size and generation. */ @@ -508,6 +814,20 @@ window_copy_sync_backing(struct window_mode_entry *wme) return (0); kept = old_hsize - collected; + old_count = old_hsize + sy; + new_count = new_hsize + sy; + if (collected != 0) { + memmove(data->outputs, data->outputs + collected, + (old_count - collected) * sizeof *data->outputs); + } + data->outputs = xreallocarray(data->outputs, new_count, + sizeof *data->outputs); + if (new_count > old_count - collected) { + memset(data->outputs + old_count - collected, 0, + (new_count - (old_count - collected)) * + sizeof *data->outputs); + } + data->output_count = new_count; if (added == 0 && collected == 0) { /* History is unchanged; only the viewport can have mutated. */ @@ -552,6 +872,7 @@ window_copy_sync_backing(struct window_mode_entry *wme) dst->cx = src->cx; dst->cy = src->cy; } + window_copy_rebuild_backing(wme); return (1); } @@ -604,18 +925,35 @@ window_copy_init(struct window_mode_entry *wme, struct window_copy_mode_data *data; struct screen *base = &wp->base; struct screen_write_ctx ctx; - u_int i, cx, cy; + u_int i, cx, cy, prompt, chosen; + int restored = 0; data = window_copy_common_init(wme); - data->backing = window_copy_clone_screen(base, &data->screen, &cx, &cy, + data->source = window_copy_clone_screen(base, &data->screen, &cx, &cy, wme->swp != wme->wp); + data->outputs = xcalloc(screen_hsize(data->source) + + screen_size_y(data->source), sizeof *data->outputs); + data->output_count = screen_hsize(data->source) + + screen_size_y(data->source); + data->output_controls = args_has(args, 'c') || args_has(args, 'U') || + options_get_number(wp->window->options, + "copy-mode-collapse-controls") == WINDOW_COPY_CONTROLS_ALWAYS; + if (args_has(args, 'c')) + window_copy_set_all_output(data, 1); + window_copy_rebuild_backing(wme); window_copy_sync_snapshot(data, base->grid); data->cx = cx; - if (cy < screen_hsize(data->backing)) { + if (data->output_controls && + window_copy_find_output(wme, cy, &prompt, &chosen)) { + restored = window_copy_restore_cursor(wme, cx, cy); + if (!restored) + restored = window_copy_restore_command_end(wme, prompt, chosen); + } + if (!restored && cy < screen_hsize(data->backing)) { data->cy = 0; data->oy = screen_hsize(data->backing) - cy; - } else { + } else if (!restored) { data->cy = cy - screen_hsize(data->backing); data->oy = 0; } @@ -685,8 +1023,14 @@ window_copy_free(struct window_mode_entry *wme) if (data->ictx != NULL) input_free(data->ictx); + if (data->source != NULL) { + screen_free(data->source); + free(data->source); + } screen_free(data->backing); free(data->backing); + free(data->outputs); + free(data->lines); screen_free(&data->screen); free(data); @@ -3000,10 +3344,16 @@ window_copy_do_refresh(struct window_mode_entry *wme, int follow) oy_from_top = screen_hsize(data->backing) - data->oy; if (!window_copy_sync_backing(wme)) { - screen_free(data->backing); - free(data->backing); - data->backing = window_copy_clone_screen(&wp->base, + screen_free(data->source); + free(data->source); + data->source = window_copy_clone_screen(&wp->base, &data->screen, NULL, NULL, wme->swp != wme->wp); + free(data->outputs); + data->outputs = xcalloc(screen_hsize(data->source) + + screen_size_y(data->source), sizeof *data->outputs); + data->output_count = screen_hsize(data->source) + + screen_size_y(data->source); + window_copy_rebuild_backing(wme); } if (follow) { @@ -3192,6 +3542,328 @@ window_copy_cmd_line_numbers_toggle(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } +static int +window_copy_find_output(struct window_mode_entry *wme, u_int target, + u_int *prompt, u_int *chosen) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->source->grid; + struct grid_line *gl; + u_int candidate = UINT_MAX, last_prompt = 0; + u_int output = UINT_MAX, y; + int command = 0, have_prompt = 0; + + *prompt = 0; + *chosen = UINT_MAX; + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) { + if (have_prompt && target >= last_prompt && target < y && + candidate != UINT_MAX) { + *prompt = last_prompt; + *chosen = candidate; + return (1); + } + last_prompt = y; + candidate = UINT_MAX; + output = UINT_MAX; + command = 0; + have_prompt = 1; + } + if (gl->flags & GRID_LINE_START_COMMAND) + command = 1; + if (gl->flags & GRID_LINE_START_OUTPUT) { + output = y; + candidate = y; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (output == UINT_MAX && command) + candidate = y; + output = UINT_MAX; + command = 0; + } + } + if (have_prompt && target >= last_prompt && candidate != UINT_MAX) { + *prompt = last_prompt; + *chosen = candidate; + return (1); + } + return (0); +} + +static void +window_copy_cursor_source(struct window_mode_entry *wme, u_int *source_x, + u_int *source_y) +{ + struct window_copy_mode_data *data = wme->data; + struct window_copy_line *line; + u_int y; + + *source_x = data->cx; + y = screen_hsize(data->backing) + data->cy - data->oy; + line = window_copy_get_line_info(data, y); + if (line == NULL) + *source_y = y; + else + *source_y = line->source_line; +} + +static int +window_copy_restore_cursor(struct window_mode_entry *wme, u_int source_x, + u_int source_y) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_line *gl; + struct window_copy_line *line; + u_int y; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + line = window_copy_get_line_info(data, y); + gl = grid_get_line(gd, y); + if (line == NULL || line->source_line != source_y || + gl->cellused == 0) + continue; + data->cx = source_x; + if (data->cx >= gl->cellused) + data->cx = gl->cellused; + if (y < gd->hsize) { + data->cy = 0; + data->oy = gd->hsize - y; + } else { + data->cy = y - gd->hsize; + data->oy = 0; + } + return (1); + } + return (0); +} + +static int +window_copy_restore_command_end(struct window_mode_entry *wme, u_int prompt, + u_int output) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_line *gl; + struct window_copy_line *line; + u_int command_start = UINT_MAX, marker = UINT_MAX; + u_int marker_x = 0, length, y; + int previous = 0; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + line = window_copy_get_line_info(data, y); + gl = grid_get_line(gd, y); + if (line == NULL) + continue; + if (line->source_line == prompt && + line->flags & WINDOW_COPY_LINE_CONTROL) + command_start = y; + if (line->source_line != output) + continue; + if (gl->flags & GRID_LINE_START_OUTPUT) { + marker = y; + marker_x = gl->osc133_data.out_start_col; + break; + } + if (marker == UINT_MAX && gl->flags & GRID_LINE_END_OUTPUT) { + marker = y; + marker_x = gl->osc133_data.out_end_col; + } + } + if (marker == UINT_MAX) + return (0); + if (marker_x != 0) { + data->cx = marker_x; + y = marker; + } else { + for (y = marker; y != 0; y--) { + if (command_start != UINT_MAX && y - 1 < command_start) + break; + length = window_copy_find_length(wme, y - 1); + if (length != 0) { + data->cx = length; + y--; + previous = 1; + break; + } + } + if (!previous) + return (0); + } + if (y < gd->hsize) { + data->cy = 0; + data->oy = gd->hsize - y; + } else { + data->cy = y - gd->hsize; + data->oy = 0; + } + return (1); +} + +static int +window_copy_restore_before(struct window_mode_entry *wme, u_int source_y) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_line *gl; + struct window_copy_line *line; + u_int length, y, last = UINT_MAX; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + line = window_copy_get_line_info(data, y); + gl = grid_get_line(gd, y); + if (line == NULL || line->source_line > source_y || + gl->cellused == 0) + continue; + last = y; + } + if (last == UINT_MAX) + return (0); + length = window_copy_find_length(wme, last); + data->cx = length; + if (last < gd->hsize) { + data->cy = 0; + data->oy = gd->hsize - last; + } else { + data->cy = last - gd->hsize; + data->oy = 0; + } + return (1); +} + +static int +window_copy_set_output(struct window_mode_entry *wme, int all, int collapse, + u_int target, int command_end) +{ + struct window_copy_mode_data *data = wme->data; + u_int prompt = 0, source_x, source_y; + u_int chosen = UINT_MAX, first_prompt, y; + int changed = 0, enabled = 0, found, initial, + wanted; + + if (data->source == NULL) + return (0); + window_copy_cursor_source(wme, &source_x, &source_y); + if (target == UINT_MAX) + target = source_y; + first_prompt = screen_hsize(data->source) + screen_size_y(data->source); + for (y = 0; y < first_prompt; y++) { + if (grid_get_line(data->source->grid, y)->flags & + GRID_LINE_START_PROMPT) { + first_prompt = y; + break; + } + } + initial = first_prompt != 0 && + first_prompt != screen_hsize(data->source) + + screen_size_y(data->source) && target < first_prompt; + if (!data->output_controls) { + data->output_controls = 1; + window_copy_rebuild_backing(wme); + enabled = 1; + } + found = window_copy_find_output(wme, target, &prompt, &chosen); + if (all) { + wanted = collapse; + if (wanted == -1) + wanted = !window_copy_any_output_collapsed(data); + changed = window_copy_set_all_output(data, wanted); + } else if (initial) { + wanted = collapse; + if (wanted == -1) + wanted = !data->top_output; + if (data->top_output != wanted) { + data->top_output = wanted; + changed = 1; + } + } else if (found && chosen != UINT_MAX) { + wanted = collapse; + if (wanted == -1) + wanted = !data->outputs[chosen]; + if (data->outputs[chosen] != wanted) { + data->outputs[chosen] = wanted; + changed = 1; + } + } + if (!changed) { + if (enabled) { + data->output_controls = 0; + window_copy_rebuild_backing(wme); + } + return (0); + } + window_copy_clear_selection(wme); + window_copy_clear_marks(wme); + window_copy_rebuild_backing(wme); + if (!command_end && + window_copy_restore_cursor(wme, source_x, source_y)) + return (1); + if (initial && window_copy_restore_before(wme, 0)) + return (1); + if (found && window_copy_restore_command_end(wme, prompt, chosen)) + return (1); + if (window_copy_restore_before(wme, source_y)) + return (1); + data->cy = screen_size_y(&data->screen) - 1; + data->cx = 0; + data->oy = 0; + return (1); +} + +static enum window_copy_cmd_action +window_copy_cmd_collapse_output(struct window_copy_cmd_state *cs) +{ + if (window_copy_set_output(cs->wme, args_has(cs->wargs, 'a'), 1, + UINT_MAX, 0)) + return (WINDOW_COPY_CMD_REDRAW); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_expand_output(struct window_copy_cmd_state *cs) +{ + if (window_copy_set_output(cs->wme, args_has(cs->wargs, 'a'), 0, + UINT_MAX, 0)) + return (WINDOW_COPY_CMD_REDRAW); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_toggle_output(struct window_copy_cmd_state *cs) +{ + struct window_copy_mode_data *data = cs->wme->data; + struct window_copy_line *line; + u_int current, current_prompt, target, target_prompt, chosen, x, y; + int command_end = 0, current_found, initial, target_found; + + if (args_has(cs->wargs, 'm')) { + if (cs->m == NULL || cmd_mouse_at(cs->wme->wp, cs->m, &x, &y, + 0) != 0 || x >= window_copy_left_margin(cs->wme)) + return (WINDOW_COPY_CMD_NOTHING); + target = screen_hsize(data->backing) + y - data->oy; + line = window_copy_get_line_info(data, target); + if (line == NULL) + return (WINDOW_COPY_CMD_NOTHING); + initial = line->flags & WINDOW_COPY_LINE_INITIAL; + window_copy_cursor_source(cs->wme, &x, ¤t); + current_found = window_copy_find_output(cs->wme, current, + ¤t_prompt, &chosen); + target_found = initial || window_copy_find_output(cs->wme, + line->source_line, &target_prompt, &chosen); + if (!target_found) + return (WINDOW_COPY_CMD_NOTHING); + command_end = initial || !current_found || + current_prompt != target_prompt; + target = line->source_line; + } else + target = UINT_MAX; + if (window_copy_set_output(cs->wme, args_has(cs->wargs, 'a'), -1, + target, command_end)) + return (WINDOW_COPY_CMD_REDRAW); + return (WINDOW_COPY_CMD_NOTHING); +} + static const struct { const char *command; u_int minargs; @@ -3474,6 +4146,24 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_NEVER, .f = window_copy_cmd_line_numbers_toggle }, + { .command = "collapse-output", + .args = { "a", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_collapse_output + }, + { .command = "expand-output", + .args = { "a", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_expand_output + }, + { .command = "toggle-output", + .args = { "am", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_toggle_output + }, { .command = "next-prompt", .args = { "o", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -3802,15 +4492,21 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, enum window_copy_cmd_action action; enum window_copy_cmd_clear clear = WINDOW_COPY_CMD_CLEAR_NEVER; const char *command; - u_int i, count = args_count(args); - int keys, flags; + u_int i, count = args_count(args), mouse_x, mouse_y; + int keys, flags, mouse_control = 0; char *error = NULL; if (count == 0) return; command = args_string(args, 0); - if (m != NULL && m->valid && !MOUSE_WHEEL(m->b)) + if (m != NULL && m->valid && !MOUSE_WHEEL(m->b) && + strcmp(command, "toggle-output") == 0 && + cmd_mouse_at(wp, m, &mouse_x, &mouse_y, 0) == 0 && + mouse_x < window_copy_left_margin(wme)) + mouse_control = 1; + if (m != NULL && m->valid && !MOUSE_WHEEL(m->b) && + !mouse_control) window_copy_move_mouse(m); cs.wme = wme; @@ -5192,10 +5888,19 @@ window_copy_line_number_width(struct window_mode_entry *wme) return (digits + 1); } +static u_int +window_copy_left_margin(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + return (window_copy_line_number_width(wme) + + (data->output_gutter ? 3 : 0)); +} + static u_int window_copy_cursor_offset(struct window_mode_entry *wme, u_int cx, u_int sx) { - u_int width = window_copy_line_number_width(wme); + u_int width = window_copy_left_margin(wme); u_int content; if (width == 0) @@ -5212,7 +5917,7 @@ window_copy_cursor_offset(struct window_mode_entry *wme, u_int cx, u_int sx) static u_int window_copy_cursor_unoffset(struct window_mode_entry *wme, u_int vx, u_int sx) { - u_int width = window_copy_line_number_width(wme); + u_int width = window_copy_left_margin(wme); u_int content; if (width == 0) @@ -5291,14 +5996,16 @@ window_copy_write_line(struct window_mode_entry *wme, struct grid_cell gc, mgc, cgc, mkgc, ln_gc, cur_ln_gc; u_int sx = screen_size_x(s); u_int hsize = screen_hsize(data->backing); - u_int width; + u_int width, line_width, gutter_width, backing_y; u_int absolute, line_number, content_sx; const char *value; char *expanded; struct format_tree *ft; int current, mode; - width = window_copy_line_number_width(wme); + line_width = window_copy_line_number_width(wme); + gutter_width = data->output_gutter ? 3 : 0; + width = line_width + gutter_width; if (width >= sx) content_sx = 1; else if (width != 0) @@ -5325,7 +6032,10 @@ window_copy_write_line(struct window_mode_entry *wme, "copy-mode-current-line-number-style", ft); cur_ln_gc.flags |= GRID_FLAG_NOPALETTE; current = (py == data->cy); - absolute = hsize - data->oy + py + 1; + backing_y = hsize - data->oy + py; + absolute = backing_y + 1; + if (data->lines != NULL && backing_y < data->line_count) + absolute = data->lines[backing_y].source_line + 1; mode = window_copy_line_number_mode(wme); if (mode == WINDOW_COPY_LINE_NUMBERS_DEFAULT) { if (py < data->oy) @@ -5340,9 +6050,39 @@ window_copy_write_line(struct window_mode_entry *wme, line_number = py - data->cy; else line_number = data->cy - py; - screen_write_cursormove(ctx, 0, py, 0); - screen_write_nputs(ctx, width, current ? &cur_ln_gc : &ln_gc, - "%*u ", (int)width - 1, line_number); + if (line_width != 0) { + screen_write_cursormove(ctx, 0, py, 0); + screen_write_nputs(ctx, line_width, + current ? &cur_ln_gc : &ln_gc, "%*u ", + (int)line_width - 1, line_number); + } + if (gutter_width != 0) { + struct grid_cell control_gc; + char control = ' '; + + if (data->lines != NULL && backing_y < data->line_count && + data->lines[backing_y].flags & WINDOW_COPY_LINE_CONTROL) { + if (data->lines[backing_y].flags & + WINDOW_COPY_LINE_INITIAL) + control = data->top_output ? '>' : 'V'; + else if (data->outputs[ + data->lines[backing_y].output_line]) + control = '>'; + else + control = 'V'; + } + screen_write_cursormove(ctx, line_width, py, 0); + screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); + if (control == ' ') + screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); + else { + memcpy(&control_gc, current ? &cur_ln_gc : &ln_gc, + sizeof control_gc); + control_gc.attr |= GRID_ATTR_CHARSET; + screen_write_putc(ctx, &control_gc, control); + } + screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); + } } window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, @@ -5417,7 +6157,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) struct screen_write_ctx ctx; u_int i; - if (window_copy_line_number_width(wme) != 0) { + if (window_copy_left_margin(wme) != 0) { screen_write_start(&ctx, &data->screen); for (i = py; i < py + ny; i++) window_copy_write_line(wme, &ctx, i); @@ -5574,8 +6314,8 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) old_cx = data->cx; old_cy = data->cy; data->cx = cx; data->cy = cy; - if (window_copy_line_numbers_active(wme)) { - width = window_copy_line_number_width(wme); + if (window_copy_left_margin(wme) != 0) { + width = window_copy_left_margin(wme); if (s->sel != NULL || data->lineflag != LINE_SEL_NONE || @@ -5808,10 +6548,10 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, style_apply(&gc, oo, "copy-mode-selection-style", ft); gc.flags |= GRID_FLAG_NOPALETTE; format_free(ft); - clipx = window_copy_line_number_width(wme); + clipx = window_copy_left_margin(wme); if (clipx >= screen_size_x(s)) clipx = screen_size_x(s) - 1; - if (window_copy_line_numbers_active(wme)) { + if (clipx != 0) { sx = window_copy_cursor_offset(wme, sx, screen_size_x(s)); endsx = window_copy_cursor_offset(wme, endsx, screen_size_x(s)); } From 07a3a16ae75fa32d4f323990a468eae9315ddc11 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 21 Jul 2026 07:42:20 +0100 Subject: [PATCH 3/8] Add vertical bar next to expanded lines. --- tty-acs.c | 2 -- window-copy.c | 32 ++++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tty-acs.c b/tty-acs.c index 62a7148f2..eedb79c2f 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -34,8 +34,6 @@ static const struct tty_acs_entry tty_acs_table[] = { { '-', "\342\206\221" }, /* arrow pointing up */ { '.', "\342\206\223" }, /* arrow pointing down */ { '0', "\342\226\256" }, /* solid square block */ - { '>', "\342\206\222" }, /* arrow pointing right */ - { 'V', "\342\206\223" }, /* arrow pointing down */ { '`', "\342\227\206" }, /* diamond */ { 'a', "\342\226\222" }, /* checker board (stipple) */ { 'b', "\342\220\211" }, diff --git a/window-copy.c b/window-copy.c index 711299daa..9c936da76 100644 --- a/window-copy.c +++ b/window-copy.c @@ -6058,18 +6058,33 @@ window_copy_write_line(struct window_mode_entry *wme, } if (gutter_width != 0) { struct grid_cell control_gc; + struct window_copy_line *line, *next; char control = ' '; - if (data->lines != NULL && backing_y < data->line_count && - data->lines[backing_y].flags & WINDOW_COPY_LINE_CONTROL) { - if (data->lines[backing_y].flags & + line = NULL; + if (data->lines != NULL && backing_y < data->line_count) + line = &data->lines[backing_y]; + if (line != NULL && + line->flags & WINDOW_COPY_LINE_CONTROL) { + if (line->flags & WINDOW_COPY_LINE_INITIAL) - control = data->top_output ? '>' : 'V'; + control = data->top_output ? '+' : '-'; else if (data->outputs[ - data->lines[backing_y].output_line]) - control = '>'; + line->output_line]) + control = '+'; else - control = 'V'; + control = '-'; + } else if (line != NULL && + line->flags & WINDOW_COPY_LINE_MEMBER) { + next = NULL; + if (backing_y + 1 < data->line_count) + next = &data->lines[backing_y + 1]; + if (next == NULL || + ~(next->flags) & WINDOW_COPY_LINE_MEMBER || + next->output_line != line->output_line) + control = 'm'; /* ACS L character */ + else + control = 'x'; /* ACS | character */ } screen_write_cursormove(ctx, line_width, py, 0); screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); @@ -6078,7 +6093,8 @@ window_copy_write_line(struct window_mode_entry *wme, else { memcpy(&control_gc, current ? &cur_ln_gc : &ln_gc, sizeof control_gc); - control_gc.attr |= GRID_ATTR_CHARSET; + if (control == 'x' || control == 'm') + control_gc.attr |= GRID_ATTR_CHARSET; screen_write_putc(ctx, &control_gc, control); } screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); From bacbb4d9d8e8c7b63b8e2f6672ee2b0d0dbd51f6 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 21 Jul 2026 15:03:52 +0100 Subject: [PATCH 4/8] Fix stray L on empty commands. --- window-copy.c | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/window-copy.c b/window-copy.c index 9c936da76..4fb944153 100644 --- a/window-copy.c +++ b/window-copy.c @@ -624,7 +624,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) struct osc133_data *osc133; u_int y, x, total, dy, prompt_dy = UINT_MAX; u_int output_line = UINT_MAX; - int collapsed = 0, command = 0, hiding = 0; + int collapsed = 0, command = 0, hiding = 0, tree = 0; int initial = 0, was_hiding; if (src == NULL) @@ -669,9 +669,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) dy = dst->grid->hsize + dst->cy; was_hiding = hiding; if (!hiding) - window_copy_set_line(data, dy, y, output_line, - output_line == UINT_MAX ? 0 : - WINDOW_COPY_LINE_MEMBER); + window_copy_set_line(data, dy, y, output_line, 0); dgl = grid_get_line(dst->grid, dy); if (initial && y == 0 && x == 0) window_copy_set_line(data, dy, y, UINT_MAX, @@ -682,7 +680,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) dgl->osc133_data.prompt_col = dst->cx; prompt_dy = dy; output_line = UINT_MAX; - collapsed = command = hiding = 0; + collapsed = command = hiding = tree = 0; } if ((sgl->flags & GRID_LINE_SECOND_PROMPT) && x == osc133->prompt_col) { @@ -700,6 +698,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) dgl->flags |= GRID_LINE_START_OUTPUT; dgl->osc133_data.out_start_col = dst->cx; output_line = y; + tree = 1; collapsed = data->outputs[y]; hiding = collapsed; if (prompt_dy != UINT_MAX) @@ -712,6 +711,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) dgl->osc133_data.exit_status = osc133->exit_status; if (output_line == UINT_MAX && command) { output_line = y; + tree = 1; collapsed = data->outputs[y]; if (prompt_dy != UINT_MAX) window_copy_set_control(data, prompt_dy, @@ -724,24 +724,33 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) command = 0; } if (was_hiding && !hiding) - window_copy_set_line(data, dy, y, output_line, - output_line == UINT_MAX ? 0 : - WINDOW_COPY_LINE_MEMBER); + window_copy_set_line(data, dy, y, output_line, 0); if (x == sgl->cellused) break; if (hiding) continue; grid_get_cell(sgd, x, y, &gc); - if (!(gc.flags & GRID_FLAG_PADDING)) + if (!(gc.flags & GRID_FLAG_PADDING)) { screen_write_cell(&ctx, &gc); + if (tree) + window_copy_set_line(data, dy, y, output_line, + WINDOW_COPY_LINE_MEMBER); + } } + if (!hiding && tree && sgl->cellused == 0 && + ~(sgl->flags) & (GRID_LINE_START_PROMPT | + GRID_LINE_START_OUTPUT | GRID_LINE_END_OUTPUT)) + window_copy_set_line(data, dy, y, output_line, + WINDOW_COPY_LINE_MEMBER); if (y + 1 != total && !(sgl->flags & GRID_LINE_WRAPPED) && (!hiding || (initial && y == 0))) { screen_write_carriagereturn(&ctx); screen_write_linefeed(&ctx, 0, 8); } - if (initial && y == 0) + if (initial && y == 0) { + tree = 1; hiding = data->top_output; + } } screen_write_stop(&ctx); if (data->backing != NULL) { From 5d4d41da5722ec6043cc2c85ecfd66ef2dbffe50 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 21 Jul 2026 18:48:16 +0100 Subject: [PATCH 5/8] Add exit status on right. --- grid.c | 2 ++ input.c | 10 ++++--- options-table.c | 8 ++++++ tmux.1 | 8 ++++++ tmux.h | 4 ++- window-copy.c | 70 +++++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 90 insertions(+), 12 deletions(-) diff --git a/grid.c b/grid.c index 0295bcf12..a16420597 100644 --- a/grid.c +++ b/grid.c @@ -1728,6 +1728,8 @@ grid_line_flags_string(int flags) strlcat(s, "START_OUTPUT,", sizeof s); if (flags & GRID_LINE_END_OUTPUT) strlcat(s, "END_OUTPUT,", sizeof s); + if (flags & GRID_LINE_END_OUTPUT_STATUS) + strlcat(s, "END_OUTPUT_STATUS,", sizeof s); if (flags & GRID_LINE_HYPERLINK) strlcat(s, "HYPERLINK,", sizeof s); if (*s == '\0') diff --git a/input.c b/input.c index 2f158ca4d..b156cb671 100644 --- a/input.c +++ b/input.c @@ -3193,13 +3193,14 @@ input_osc_112(struct input_ctx *ictx, const char *p) /* Parse the OSC 133 D exit status. */ static int -input_osc_133_exit_status(const char *p) +input_osc_133_exit_status(const char *p, int *present) { const char *end; char *copy; const char *errstr; long long status; + *present = 0; if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2) return (0); end = strchr(p + 2, ';'); @@ -3213,6 +3214,7 @@ input_osc_133_exit_status(const char *p) free(copy); return (0); } + *present = 1; status = strtonum(copy, 0, 255, &errstr); free(copy); if (errstr != NULL) @@ -3270,7 +3272,7 @@ input_osc_133(struct input_ctx *ictx, const char *p) u_int line = s->cy + gd->hsize; struct grid_line *gl = NULL; const char *cp; - int status; + int status, status_present; if (line < gd->hsize + gd->sy) gl = grid_get_line(gd, line); @@ -3327,7 +3329,7 @@ input_osc_133(struct input_ctx *ictx, const char *p) } break; case 'D': - status = input_osc_133_exit_status(p); + status = input_osc_133_exit_status(p, &status_present); if (wp != NULL) { wp->cmd_end_time = time(NULL); wp->flags &= ~PANE_CMDRUNNING; @@ -3336,6 +3338,8 @@ input_osc_133(struct input_ctx *ictx, const char *p) } if (gl != NULL) { gl->flags |= GRID_LINE_END_OUTPUT; + if (status_present) + gl->flags |= GRID_LINE_END_OUTPUT_STATUS; gl->osc133_data.out_end_col = s->cx; gl->osc133_data.exit_status = status; } diff --git a/options-table.c b/options-table.c index 35dada347..6587634f6 100644 --- a/options-table.c +++ b/options-table.c @@ -1384,6 +1384,14 @@ const struct options_table_entry options_table[] = { .text = "When OSC 133 output collapse controls are shown in copy mode." }, + { .name = "copy-mode-exit-status-format", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, + .default_str = "#[align=right]" + "#{?exit_status_present,#[bg=themeyellow](#{exit_status}),}", + .text = "Format of OSC 133 exit status in copy mode." + }, + { .name = "copy-mode-current-match-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/tmux.1 b/tmux.1 index 7bc589737..c313e7747 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5777,6 +5777,14 @@ section. .It Ic copy\-mode\-position\-format Ar format Format of the position indicator in copy mode. .Pp +.It Ic copy\-mode\-exit\-status\-format Ar format +Format of the OSC 133 exit status shown beside commands in copy mode. +The format is expanded with +.Ql exit_status +and +.Ql exit_status_present , +which is set only when the OSC 133 end-of-output marker includes a status. +.Pp .It Ic copy\-mode\-position\-style Ar style Set the style of the position indicator in copy mode. For how to specify diff --git a/tmux.h b/tmux.h index f030610eb..6f182d803 100644 --- a/tmux.h +++ b/tmux.h @@ -814,6 +814,7 @@ struct colour_palette { #define GRID_LINE_START_OUTPUT 0x40 #define GRID_LINE_END_OUTPUT 0x80 #define GRID_LINE_HYPERLINK 0x100 +#define GRID_LINE_END_OUTPUT_STATUS 0x200 /* All OSC 133 flags. */ #define GRID_LINE_OSC133_FLAGS \ @@ -821,7 +822,8 @@ struct colour_palette { GRID_LINE_SECOND_PROMPT| \ GRID_LINE_START_COMMAND| \ GRID_LINE_START_OUTPUT| \ - GRID_LINE_END_OUTPUT) + GRID_LINE_END_OUTPUT| \ + GRID_LINE_END_OUTPUT_STATUS) /* Grid string flags. */ #define GRID_STRING_WITH_SEQUENCES 0x1 diff --git a/window-copy.c b/window-copy.c index 4fb944153..9dee2d028 100644 --- a/window-copy.c +++ b/window-copy.c @@ -272,6 +272,8 @@ struct window_copy_line { u_int source_line; u_int output_line; u_char flags; + u_char exit_status; + u_char exit_status_present; }; /* @@ -515,6 +517,19 @@ window_copy_set_control(struct window_copy_mode_data *data, u_int y, line->flags |= WINDOW_COPY_LINE_CONTROL; } +static void +window_copy_set_exit_status(struct window_copy_mode_data *data, u_int y, + u_char exit_status) +{ + struct window_copy_line *line; + + line = window_copy_get_line_info(data, y); + if (line == NULL) + return; + line->exit_status = exit_status; + line->exit_status_present = 1; +} + static int window_copy_any_output_collapsed(struct window_copy_mode_data *data) { @@ -623,6 +638,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) struct grid_cell gc; struct osc133_data *osc133; u_int y, x, total, dy, prompt_dy = UINT_MAX; + u_int end_prompt_dy; u_int output_line = UINT_MAX; int collapsed = 0, command = 0, hiding = 0, tree = 0; int initial = 0, was_hiding; @@ -667,6 +683,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) osc133 = &sgl->osc133_data; for (x = 0; x <= sgl->cellused; x++) { dy = dst->grid->hsize + dst->cy; + end_prompt_dy = prompt_dy; was_hiding = hiding; if (!hiding) window_copy_set_line(data, dy, y, output_line, 0); @@ -707,6 +724,8 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) if ((sgl->flags & GRID_LINE_END_OUTPUT) && x == osc133->out_end_col) { dgl->flags |= GRID_LINE_END_OUTPUT; + if (sgl->flags & GRID_LINE_END_OUTPUT_STATUS) + dgl->flags |= GRID_LINE_END_OUTPUT_STATUS; dgl->osc133_data.out_end_col = dst->cx; dgl->osc133_data.exit_status = osc133->exit_status; if (output_line == UINT_MAX && command) { @@ -717,6 +736,10 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) window_copy_set_control(data, prompt_dy, output_line); } + if (end_prompt_dy != UINT_MAX && + sgl->flags & GRID_LINE_END_OUTPUT_STATUS) + window_copy_set_exit_status(data, end_prompt_dy, + osc133->exit_status); if (initial && prompt_dy == UINT_MAX) hiding = data->top_output; else @@ -6008,8 +6031,9 @@ window_copy_write_line(struct window_mode_entry *wme, u_int width, line_width, gutter_width, backing_y; u_int absolute, line_number, content_sx; const char *value; - char *expanded; + char *expanded, *exit_status, *position; struct format_tree *ft; + struct window_copy_line *status_line; int current, mode; line_width = window_copy_line_number_width(wme); @@ -6021,6 +6045,7 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx = sx - width; else content_sx = sx; + backing_y = hsize - data->oy + py; screen_write_cursormove(ctx, 0, py, 0); @@ -6041,7 +6066,6 @@ window_copy_write_line(struct window_mode_entry *wme, "copy-mode-current-line-number-style", ft); cur_ln_gc.flags |= GRID_FLAG_NOPALETTE; current = (py == data->cy); - backing_y = hsize - data->oy + py; absolute = backing_y + 1; if (data->lines != NULL && backing_y < data->line_count) absolute = data->lines[backing_y].source_line + 1; @@ -6113,18 +6137,48 @@ window_copy_write_line(struct window_mode_entry *wme, window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, content_sx, &mgc, &cgc, &mkgc); + exit_status = NULL; + status_line = window_copy_get_line_info(data, backing_y); + if (data->output_gutter && status_line != NULL && + status_line->exit_status_present) { + format_add(ft, "exit_status", "%hhu", status_line->exit_status); + format_add(ft, "exit_status_present", "1"); + value = options_get_string(oo, "copy-mode-exit-status-format"); + if (*value != '\0') { + exit_status = format_expand(ft, value); + if (*exit_status == '\0') { + free(exit_status); + exit_status = NULL; + } + } + } + + position = NULL; if (py == 0 && s->rupper < s->rlower && !data->hide_position) { value = options_get_string(oo, "copy-mode-position-format"); if (*value != '\0') { - expanded = format_expand(ft, value); - if (*expanded != '\0') { - screen_write_cursormove(ctx, width, 0, 0); - format_draw(ctx, &gc, content_sx, expanded, - NULL, 0); + position = format_expand(ft, value); + if (*position == '\0') { + free(position); + position = NULL; } - free(expanded); } } + if (exit_status != NULL && position != NULL) + xasprintf(&expanded, "%s %s", exit_status, position); + else if (exit_status != NULL) + expanded = xstrdup(exit_status); + else if (position != NULL) + expanded = xstrdup(position); + else + expanded = NULL; + if (expanded != NULL) { + screen_write_cursormove(ctx, width, py, 0); + format_draw(ctx, &gc, content_sx, expanded, NULL, 0); + free(expanded); + } + free(exit_status); + free(position); if (py == data->cy && data->cx >= content_sx) { screen_write_cursormove(ctx, window_copy_cursor_offset(wme, From f44246c764aba19b21ab079e78a3af72d3dcec65 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 21 Jul 2026 19:19:31 +0100 Subject: [PATCH 6/8] Add highlighting to the current line. --- options-table.c | 4 +++- tmux.1 | 3 +++ window-copy.c | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/options-table.c b/options-table.c index 6587634f6..e40553630 100644 --- a/options-table.c +++ b/options-table.c @@ -1388,7 +1388,9 @@ const struct options_table_entry options_table[] = { .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, .default_str = "#[align=right]" - "#{?exit_status_present,#[bg=themeyellow](#{exit_status}),}", + "#{?exit_status_present,#[bg=themeyellow]#[fg=themeblack]" + "#{?exit_status_current,#[fg=themewhite],}(#{exit_status})" + "#[bg=themeyellow]#[fg=themeblack],}", .text = "Format of OSC 133 exit status in copy mode." }, diff --git a/tmux.1 b/tmux.1 index c313e7747..e5034a385 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5784,6 +5784,9 @@ The format is expanded with and .Ql exit_status_present , which is set only when the OSC 133 end-of-output marker includes a status. +The +.Ql exit_status_current +format is set when the cursor is on the command line. .Pp .It Ic copy\-mode\-position\-style Ar style Set the style of the position indicator in copy mode. diff --git a/window-copy.c b/window-copy.c index 9dee2d028..9a663e120 100644 --- a/window-copy.c +++ b/window-copy.c @@ -6046,6 +6046,7 @@ window_copy_write_line(struct window_mode_entry *wme, else content_sx = sx; backing_y = hsize - data->oy + py; + current = (py == data->cy); screen_write_cursormove(ctx, 0, py, 0); @@ -6065,7 +6066,6 @@ window_copy_write_line(struct window_mode_entry *wme, style_apply(&cur_ln_gc, oo, "copy-mode-current-line-number-style", ft); cur_ln_gc.flags |= GRID_FLAG_NOPALETTE; - current = (py == data->cy); absolute = backing_y + 1; if (data->lines != NULL && backing_y < data->line_count) absolute = data->lines[backing_y].source_line + 1; @@ -6143,6 +6143,7 @@ window_copy_write_line(struct window_mode_entry *wme, status_line->exit_status_present) { format_add(ft, "exit_status", "%hhu", status_line->exit_status); format_add(ft, "exit_status_present", "1"); + format_add(ft, "exit_status_current", "%d", current); value = options_get_string(oo, "copy-mode-exit-status-format"); if (*value != '\0') { exit_status = format_expand(ft, value); From f0eaa5e9d6f8ee893afb6d4ed49d8cdc61089c0d Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Wed, 22 Jul 2026 11:33:46 +0100 Subject: [PATCH 7/8] Remove code to put exit-code on right and go for just simple ! on left of +. --- options-table.c | 19 +----- tmux.1 | 28 ++------ window-copy.c | 177 ++++++++++++++++++++++++++---------------------- 3 files changed, 106 insertions(+), 118 deletions(-) diff --git a/options-table.c b/options-table.c index e40553630..f5966939d 100644 --- a/options-table.c +++ b/options-table.c @@ -114,10 +114,6 @@ static const char *options_table_theme_list[] = { static const char *options_table_copy_mode_line_numbers_list[] = { "off", "default", "absolute", "relative", "hybrid", NULL }; -static const char *options_table_copy_mode_collapse_controls_list[] = { - "always", "never", "on-demand", NULL -}; - /* Status line format. */ #define OPTIONS_TABLE_STATUS_FORMAT1 \ "#[align=left range=left #{E:status-left-style}]" \ @@ -1376,22 +1372,11 @@ const struct options_table_entry options_table[] = { .text = "Style of search matches in copy mode." }, - { .name = "copy-mode-collapse-controls", - .type = OPTIONS_TABLE_CHOICE, - .scope = OPTIONS_TABLE_WINDOW, - .choices = options_table_copy_mode_collapse_controls_list, - .default_num = 2, - .text = "When OSC 133 output collapse controls are shown in copy mode." - }, - { .name = "copy-mode-exit-status-format", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, - .default_str = "#[align=right]" - "#{?exit_status_present,#[bg=themeyellow]#[fg=themeblack]" - "#{?exit_status_current,#[fg=themewhite],}(#{exit_status})" - "#[bg=themeyellow]#[fg=themeblack],}", - .text = "Format of OSC 133 exit status in copy mode." + .default_str = "#{?exit_status,#[fg=themered]!, }", + .text = "Format of OSC 133 exit status indicator in copy mode." }, { .name = "copy-mode-current-match-style", diff --git a/tmux.1 b/tmux.1 index e5034a385..763abda0a 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5778,15 +5778,18 @@ section. Format of the position indicator in copy mode. .Pp .It Ic copy\-mode\-exit\-status\-format Ar format -Format of the OSC 133 exit status shown beside commands in copy mode. +Format of the OSC 133 exit status indicator in the copy-mode gutter. +The default displays a theme red +.Ql ! +for a nonzero status and a space otherwise. +The gutter grows to fit the expanded format. +The format is drawn to the left of the collapse control, and its alignment +applies within that area. The format is expanded with .Ql exit_status and .Ql exit_status_present , which is set only when the OSC 133 end-of-output marker includes a status. -The -.Ql exit_status_current -format is set when the cursor is on the command line. .Pp .It Ic copy\-mode\-position\-style Ar style Set the style of the position indicator in copy mode. @@ -5850,23 +5853,6 @@ and .Ic hybrid , they use absolute line numbers. .Pp -.It Xo Ic copy\-mode\-collapse\-controls -.Op Ic always | never | on\-demand -.Xc -Choose when OSC 133 output collapse controls are shown in copy mode. -.Ic always -shows the control column in every copy-mode view; -.Ic never -does not show it, although collapse key bindings and commands still work; and -.Ic on\-demand -shows it after entering copy mode with -.Fl U -or -.Fl c , -or after output is changed with a copy-mode command or key binding. -The default is -.Ic on\-demand . -.Pp .It Xo Ic mode\-keys .Op Ic vi | emacs .Xc diff --git a/window-copy.c b/window-copy.c index 9a663e120..2b68b9b0c 100644 --- a/window-copy.c +++ b/window-copy.c @@ -28,13 +28,10 @@ struct window_copy_mode_data; -#define WINDOW_COPY_CONTROLS_ALWAYS 0 -#define WINDOW_COPY_CONTROLS_NEVER 1 -#define WINDOW_COPY_CONTROLS_ON_DEMAND 2 - #define WINDOW_COPY_LINE_CONTROL 0x1 #define WINDOW_COPY_LINE_MEMBER 0x2 #define WINDOW_COPY_LINE_INITIAL 0x4 +#define WINDOW_COPY_LINE_OUTPUT 0x8 static const char *window_copy_key_table(struct window_mode_entry *); static void window_copy_command(struct window_mode_entry *, struct client *, @@ -72,7 +69,6 @@ static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); static u_int window_copy_line_number_width(struct window_mode_entry *); static u_int window_copy_left_margin(struct window_mode_entry *); -static int window_copy_controls_visible(struct window_mode_entry *); static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, u_int); static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, @@ -335,6 +331,7 @@ struct window_copy_mode_data { int output_gutter; int output_controls; int top_output; + u_int output_status_width; enum { SEL_CHAR, /* select one char at a time */ @@ -506,7 +503,7 @@ window_copy_set_line(struct window_copy_mode_data *data, u_int y, static void window_copy_set_control(struct window_copy_mode_data *data, u_int y, - u_int output_line) + u_int output_line, int output) { struct window_copy_line *line; @@ -515,6 +512,8 @@ window_copy_set_control(struct window_copy_mode_data *data, u_int y, return; line->output_line = output_line; line->flags |= WINDOW_COPY_LINE_CONTROL; + if (output) + line->flags |= WINDOW_COPY_LINE_OUTPUT; } static void @@ -530,6 +529,56 @@ window_copy_set_exit_status(struct window_copy_mode_data *data, u_int y, line->exit_status_present = 1; } +static char * +window_copy_expand_exit_status(struct window_mode_entry *wme, + struct window_copy_line *line) +{ + struct format_tree *ft; + const char *value; + char *expanded; + + /* + * This is called while rebuilding the backing grid, before the copy + * cursor and viewport have been restored for the new grid. + */ + ft = format_create(NULL, NULL, 0, 0); + format_add(ft, "exit_status", "0"); + format_add(ft, "exit_status_present", "0"); + if (line->exit_status_present) { + format_add(ft, "exit_status", "%hhu", line->exit_status); + format_add(ft, "exit_status_present", "1"); + } + value = options_get_string(wme->wp->window->options, + "copy-mode-exit-status-format"); + expanded = format_expand(ft, value); + format_free(ft); + return (expanded); +} + +static void +window_copy_update_exit_status_width(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + struct window_copy_line *line; + char *expanded; + u_int width, y; + + data->output_status_width = 0; + if (!data->output_gutter || data->lines == NULL) + return; + width = 1; + for (y = 0; y < data->line_count; y++) { + line = &data->lines[y]; + if (~line->flags & WINDOW_COPY_LINE_CONTROL) + continue; + expanded = window_copy_expand_exit_status(wme, line); + if (format_width(expanded) > width) + width = format_width(expanded); + free(expanded); + } + data->output_status_width = width; +} + static int window_copy_any_output_collapsed(struct window_copy_mode_data *data) { @@ -613,19 +662,6 @@ window_copy_set_all_output(struct window_copy_mode_data *data, int collapse) return (changed); } -static int -window_copy_controls_visible(struct window_mode_entry *wme) -{ - struct window_copy_mode_data *data = wme->data; - int controls; - - controls = options_get_number(wme->wp->window->options, - "copy-mode-collapse-controls"); - return (controls == WINDOW_COPY_CONTROLS_ALWAYS || - (controls == WINDOW_COPY_CONTROLS_ON_DEMAND && - data->output_controls)); -} - /* Recreate the backing screen with any collapsed OSC 133 output omitted. */ static void window_copy_rebuild_backing(struct window_mode_entry *wme) @@ -656,6 +692,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) data->line_count = 0; data->backing = dst; data->output_gutter = 0; + data->output_status_width = 0; return; } sgd = src->grid; @@ -719,7 +756,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) collapsed = data->outputs[y]; hiding = collapsed; if (prompt_dy != UINT_MAX) - window_copy_set_control(data, prompt_dy, output_line); + window_copy_set_control(data, prompt_dy, output_line, 1); } if ((sgl->flags & GRID_LINE_END_OUTPUT) && x == osc133->out_end_col) { @@ -734,7 +771,7 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) collapsed = data->outputs[y]; if (prompt_dy != UINT_MAX) window_copy_set_control(data, prompt_dy, - output_line); + output_line, 0); } if (end_prompt_dy != UINT_MAX && sgl->flags & GRID_LINE_END_OUTPUT_STATUS) @@ -781,7 +818,8 @@ window_copy_rebuild_backing(struct window_mode_entry *wme) free(data->backing); } data->backing = dst; - data->output_gutter = window_copy_controls_visible(wme); + data->output_gutter = data->output_controls; + window_copy_update_exit_status_width(wme); } /* @@ -967,9 +1005,7 @@ window_copy_init(struct window_mode_entry *wme, screen_size_y(data->source), sizeof *data->outputs); data->output_count = screen_hsize(data->source) + screen_size_y(data->source); - data->output_controls = args_has(args, 'c') || args_has(args, 'U') || - options_get_number(wp->window->options, - "copy-mode-collapse-controls") == WINDOW_COPY_CONTROLS_ALWAYS; + data->output_controls = args_has(args, 'c') || args_has(args, 'U'); if (args_has(args, 'c')) window_copy_set_all_output(data, 1); window_copy_rebuild_backing(wme); @@ -5924,9 +5960,12 @@ static u_int window_copy_left_margin(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; + u_int width = 0; - return (window_copy_line_number_width(wme) + - (data->output_gutter ? 3 : 0)); + if (data->output_gutter) + width = data->output_status_width + 2; + + return (window_copy_line_number_width(wme) + width); } static u_int @@ -6031,13 +6070,12 @@ window_copy_write_line(struct window_mode_entry *wme, u_int width, line_width, gutter_width, backing_y; u_int absolute, line_number, content_sx; const char *value; - char *expanded, *exit_status, *position; + char *expanded, *status; struct format_tree *ft; - struct window_copy_line *status_line; int current, mode; line_width = window_copy_line_number_width(wme); - gutter_width = data->output_gutter ? 3 : 0; + gutter_width = data->output_gutter ? data->output_status_width + 2 : 0; width = line_width + gutter_width; if (width >= sx) content_sx = 1; @@ -6045,9 +6083,6 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx = sx - width; else content_sx = sx; - backing_y = hsize - data->oy + py; - current = (py == data->cy); - screen_write_cursormove(ctx, 0, py, 0); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -6066,6 +6101,8 @@ window_copy_write_line(struct window_mode_entry *wme, style_apply(&cur_ln_gc, oo, "copy-mode-current-line-number-style", ft); cur_ln_gc.flags |= GRID_FLAG_NOPALETTE; + current = (py == data->cy); + backing_y = hsize - data->oy + py; absolute = backing_y + 1; if (data->lines != NULL && backing_y < data->line_count) absolute = data->lines[backing_y].source_line + 1; @@ -6093,6 +6130,7 @@ window_copy_write_line(struct window_mode_entry *wme, struct grid_cell control_gc; struct window_copy_line *line, *next; char control = ' '; + u_int i; line = NULL; if (data->lines != NULL && backing_y < data->line_count) @@ -6100,13 +6138,15 @@ window_copy_write_line(struct window_mode_entry *wme, if (line != NULL && line->flags & WINDOW_COPY_LINE_CONTROL) { if (line->flags & - WINDOW_COPY_LINE_INITIAL) + WINDOW_COPY_LINE_INITIAL) { control = data->top_output ? '+' : '-'; - else if (data->outputs[ - line->output_line]) - control = '+'; - else - control = '-'; + } else if (line->flags & WINDOW_COPY_LINE_OUTPUT && + line->output_line < data->output_count) { + if (data->outputs[line->output_line]) + control = '+'; + else + control = '-'; + } } else if (line != NULL && line->flags & WINDOW_COPY_LINE_MEMBER) { next = NULL; @@ -6120,66 +6160,42 @@ window_copy_write_line(struct window_mode_entry *wme, control = 'x'; /* ACS | character */ } screen_write_cursormove(ctx, line_width, py, 0); - screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); - if (control == ' ') + for (i = 0; i < gutter_width; i++) screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); - else { + if (control != ' ') { + screen_write_cursormove(ctx, + line_width + data->output_status_width, py, 0); memcpy(&control_gc, current ? &cur_ln_gc : &ln_gc, sizeof control_gc); if (control == 'x' || control == 'm') control_gc.attr |= GRID_ATTR_CHARSET; screen_write_putc(ctx, &control_gc, control); } - screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); + if (line != NULL && + line->flags & WINDOW_COPY_LINE_CONTROL) { + status = window_copy_expand_exit_status(wme, line); + screen_write_cursormove(ctx, line_width, py, 0); + format_draw(ctx, current ? &cur_ln_gc : &ln_gc, + data->output_status_width, status, NULL, 0); + free(status); + } } } window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, content_sx, &mgc, &cgc, &mkgc); - exit_status = NULL; - status_line = window_copy_get_line_info(data, backing_y); - if (data->output_gutter && status_line != NULL && - status_line->exit_status_present) { - format_add(ft, "exit_status", "%hhu", status_line->exit_status); - format_add(ft, "exit_status_present", "1"); - format_add(ft, "exit_status_current", "%d", current); - value = options_get_string(oo, "copy-mode-exit-status-format"); - if (*value != '\0') { - exit_status = format_expand(ft, value); - if (*exit_status == '\0') { - free(exit_status); - exit_status = NULL; - } - } - } - - position = NULL; if (py == 0 && s->rupper < s->rlower && !data->hide_position) { value = options_get_string(oo, "copy-mode-position-format"); if (*value != '\0') { - position = format_expand(ft, value); - if (*position == '\0') { - free(position); - position = NULL; + expanded = format_expand(ft, value); + if (*expanded != '\0') { + screen_write_cursormove(ctx, width, 0, 0); + format_draw(ctx, &gc, content_sx, expanded, NULL, 0); } + free(expanded); } } - if (exit_status != NULL && position != NULL) - xasprintf(&expanded, "%s %s", exit_status, position); - else if (exit_status != NULL) - expanded = xstrdup(exit_status); - else if (position != NULL) - expanded = xstrdup(position); - else - expanded = NULL; - if (expanded != NULL) { - screen_write_cursormove(ctx, width, py, 0); - format_draw(ctx, &gc, content_sx, expanded, NULL, 0); - free(expanded); - } - free(exit_status); - free(position); if (py == data->cy && data->cx >= content_sx) { screen_write_cursormove(ctx, window_copy_cursor_offset(wme, @@ -6278,6 +6294,7 @@ window_copy_style_changed(struct window_mode_entry *wme) if (data->screen.sel != NULL) window_copy_set_selection(wme, 0, 1); + window_copy_update_exit_status_width(wme); window_copy_redraw_screen(wme); } From 473eca949a41ed71c53f8b6224d2141f56846ab9 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Wed, 22 Jul 2026 11:35:28 +0100 Subject: [PATCH 8/8] Add regression test for copy-mode-exit-status. --- regress/copy-mode-exit-status.sh | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 regress/copy-mode-exit-status.sh diff --git a/regress/copy-mode-exit-status.sh b/regress/copy-mode-exit-status.sh new file mode 100644 index 000000000..5dbbc6ced --- /dev/null +++ b/regress/copy-mode-exit-status.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +# Check OSC 133 exit status indicators in copy mode using a real client. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +fail() { + echo "$*" >&2 + exit 1 +} + +capture() { + $TMUX capture-pane -pS0 -E- >$TMP || exit 1 +} + +check_grep() { + grep -Fq "$1" $TMP || fail "missing pattern: $1" +} + +check_no_grep() { + grep -Fq "$1" $TMP && fail "unexpected pattern: $1" +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15 + +$TMUX2 new-session -d -x80 -y10 \ + "printf '\033]133;A\007p\$ \033]133;B\007one\n\033]133;C\007out1\n\033]133;D;0\007\033]133;A\007p\$ \033]133;B\007two\n\033]133;C\007out2\n\033]133;D;123\007\033]133;A\007p\$ \033]133;B\007three\n\033]133;C\007out3\n\033]133;D\007\033]133;A\007p\$ \033]133;B\007'; exec sleep 100" || \ + exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g copy-mode-position-format '#[align=left]POS' || exit 1 + +$TMUX new-session -d -x80 -y10 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +$TMUX2 copy-mode -U || exit 1 +$TMUX2 send -X history-top || exit 1 +sleep 1 +capture +check_grep "!- p\$ two" +check_grep "POS" + +# Rebuilding with collapsed output must not expand copy-mode formats before +# the viewport has been restored for the shorter backing grid. +$TMUX2 send -X collapse-output -a || exit 1 +sleep 1 +capture +check_grep "!+ p\$ two" + +# A format wider than the standard three-column gutter is not truncated. +$TMUX2 send -X cancel || exit 1 +$TMUX2 set -g copy-mode-exit-status-format \ + '#[align=right]#{?exit_status,!#{exit_status}, }' || exit 1 +$TMUX2 copy-mode -c || exit 1 +$TMUX2 send -X history-top || exit 1 +sleep 1 +capture +check_grep "!123+ p\$ two" + +$TMUX2 send -X select-line || exit 1 +$TMUX2 send -X copy-selection || exit 1 +selection=$($TMUX2 show-buffer) +case $selection in +*RC=*) fail "exit status was copied" ;; +esac + +exit 0