From 2743f14ebaac8d60b1ce5c77ddfa83a2bdc106c7 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Sat, 18 Jul 2026 15:49:01 +0100 Subject: [PATCH] Add ability to collapse commmands to copy-mode. --- cmd-copy-mode.c | 4 +- grid.c | 6 + input.c | 19 +- key-bindings.c | 8 +- options-table.c | 22 ++ regress/collapse-output.sh | 34 +++ tmux.1 | 57 ++++- tmux.h | 11 + window-copy.c | 441 +++++++++++++++++++++++++++++++++++-- 9 files changed, 578 insertions(+), 24 deletions(-) create mode 100644 regress/collapse-output.sh 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/grid.c b/grid.c index d753e03f1..bdceda547 100644 --- a/grid.c +++ b/grid.c @@ -1703,6 +1703,12 @@ grid_line_flags_string(int flags) strlcat(s, "START_PROMPT,", sizeof s); if (flags & GRID_LINE_START_OUTPUT) strlcat(s, "START_OUTPUT,", sizeof s); + if (flags & GRID_LINE_START_COMMAND) + strlcat(s, "START_COMMAND,", sizeof s); + if (flags & GRID_LINE_END_OUTPUT) + strlcat(s, "END_OUTPUT,", sizeof s); + if (flags & GRID_LINE_OUTPUT_COLLAPSED) + strlcat(s, "OUTPUT_COLLAPSED,", 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..405b279c1 100644 --- a/input.c +++ b/input.c @@ -3239,6 +3239,7 @@ 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; + u_int column = ictx->ctx.s->cx; struct grid_line *gl = NULL; const char *errstr; int status; @@ -3248,16 +3249,26 @@ input_osc_133(struct input_ctx *ictx, const char *p) switch (*p) { case 'A': - if (gl != NULL) + if (gl != NULL && !(gl->flags & GRID_LINE_START_PROMPT)) { gl->flags |= GRID_LINE_START_PROMPT; + gl->prompt = column; + } if (wp != NULL) { wp->last_prompt_time = time(NULL); events_fire_pane("pane-shell-prompt", wp); } break; + case 'B': + if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) { + gl->flags |= GRID_LINE_START_COMMAND; + gl->command = column; + } + break; case 'C': - if (gl != NULL) + if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) { gl->flags |= GRID_LINE_START_OUTPUT; + gl->output = column; + } if (wp != NULL) { wp->cmd_start_time = time(NULL); wp->cmd_end_time = 0; @@ -3267,6 +3278,10 @@ input_osc_133(struct input_ctx *ictx, const char *p) } break; case 'D': + if (gl != NULL) { + gl->flags |= GRID_LINE_END_OUTPUT; + gl->end_output = column; + } if (wp != NULL) { wp->cmd_end_time = time(NULL); wp->flags &= ~PANE_CMDRUNNING; diff --git a/key-bindings.c b/key-bindings.c index ff6111870..06852a0f4 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 C-Tab { 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 C-Tab { 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 d0a175b9f..e22b5db35 100644 --- a/options-table.c +++ b/options-table.c @@ -114,6 +114,12 @@ 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_collapse_output_list[] = { + "output-only", "output-and-prompt", NULL +}; +static const char *options_table_copy_mode_collapse_controls_list[] = { + "always", "never", "on-demand", NULL +}; /* Status line format. */ #define OPTIONS_TABLE_STATUS_FORMAT1 \ @@ -1363,6 +1369,22 @@ const struct options_table_entry options_table[] = { .text = "Time for which 'display-panes' should show pane numbers." }, + { .name = "collapse-output", + .type = OPTIONS_TABLE_CHOICE, + .scope = OPTIONS_TABLE_WINDOW, + .choices = options_table_collapse_output_list, + .default_num = 0, + .text = "What is hidden when command output is collapsed 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 collapse controls are shown in copy mode." + }, + { .name = "copy-mode-match-style", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/regress/collapse-output.sh b/regress/collapse-output.sh new file mode 100644 index 000000000..5041af81e --- /dev/null +++ b/regress/collapse-output.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007p\\$ \\033]133;B\\007echo hi\\033]133;C\\007\\nhello\\n\\033]133;D;0\\007\"; exec sleep 100'" || exit 1 +sleep 1 + +$TMUX copy-mode -c || exit 1 +$TMUX send-keys -X search-backward hello || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != hello ] || exit 1 + +$TMUX send-keys C-Tab || exit 1 +$TMUX send-keys -X search-backward hello || exit 1 +shown=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$shown" = hello ] || exit 1 +$TMUX send-keys -X select-line || exit 1 +$TMUX send-keys -X copy-selection || exit 1 +selected=$($TMUX show-buffer) +[ "$selected" = hello ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + +$TMUX set-window-option collapse-output output-and-prompt || exit 1 +$TMUX copy-mode -c || exit 1 +$TMUX send-keys -X search-backward 'p$' || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != 'p$ echo hi' ] || exit 1 + +$TMUX kill-server diff --git a/tmux.1 b/tmux.1 index fe3951735..df126b7bb 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2167,6 +2167,25 @@ 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. +.It Xo +.Ic uncollapse\-output +.Op Fl a +.Xc +Show the output for the command on the cursor line. +With +.Fl a , +show all command output in the copy-mode buffer. +.Pp +The Tab key toggles output for the command on the cursor line and C-Tab +toggles all command output. +.It Xo .Ic middle\-line (vi: M) (emacs: M\-r) @@ -2595,12 +2614,20 @@ 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 all OSC 133 command output collapsed. +.Pp +.Fl U +enters copy mode with OSC 133 output controls shown and all output uncollapsed. +The controls are also shown when output is changed with a copy-mode command or +key binding. +.Pp .Fl u enters copy mode and scrolls one page up and .Fl d @@ -5811,6 +5838,34 @@ and .Ic hybrid , they use absolute line numbers. .Pp +.It Xo Ic collapse\-output +.Op Ic output\-only | output\-and\-prompt +.Xc +Choose what is hidden when OSC 133 command output is collapsed in copy mode. +.Ic output\-only +hides only the output between the C and D markers; +.Ic output\-and\-prompt +also hides the prompt between the A and B markers. +The default is +.Ic output\-only . +.Pp +.It Xo Ic copy\-mode\-collapse\-controls +.Op Ic always | never | on\-demand +.Xc +Choose when the 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/tmux.h b/tmux.h index b0cbb8520..5b8f21393 100644 --- a/tmux.h +++ b/tmux.h @@ -811,6 +811,11 @@ struct colour_palette { #define GRID_LINE_START_PROMPT 0x8 #define GRID_LINE_START_OUTPUT 0x10 #define GRID_LINE_HYPERLINK 0x20 +#define GRID_LINE_START_COMMAND 0x40 +#define GRID_LINE_END_OUTPUT 0x80 +#define GRID_LINE_OUTPUT_COLLAPSED 0x100 +#define GRID_LINE_OUTPUT_CONTROL 0x200 +#define GRID_LINE_OUTPUT_MEMBER 0x400 /* Grid string flags. */ #define GRID_STRING_WITH_SEQUENCES 0x1 @@ -889,6 +894,12 @@ struct grid_line { u_int extdsize; int flags; + u_int prompt; + u_int command; + u_int output; + u_int end_output; + u_int source_line; + u_int output_line; time_t time; }; diff --git a/window-copy.c b/window-copy.c index a6909a8d4..e34970daa 100644 --- a/window-copy.c +++ b/window-copy.c @@ -28,6 +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 + 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 +67,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, @@ -74,6 +80,8 @@ static void window_copy_write_lines(struct window_mode_entry *, 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); +static void window_copy_rebuild_backing(struct window_mode_entry *); +static int window_copy_set_output(struct window_mode_entry *, int, int); static int window_copy_search_compare(struct grid *, u_int, u_int, struct grid *, u_int, int); static int window_copy_search_lr(struct grid *, struct grid *, u_int *, @@ -266,6 +274,7 @@ struct window_copy_mode_data { struct screen screen; struct screen *backing; + struct screen *source; int backing_written; /* backing display started */ struct input_ctx *ictx; @@ -299,6 +308,8 @@ 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; enum { SEL_CHAR, /* select one char at a time */ @@ -446,6 +457,165 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx, return (dst); } +static int +window_copy_any_output_collapsed(struct screen *s) +{ + struct grid *gd = s->grid; + u_int y; + + for (y = 0; y < gd->hsize + gd->sy; y++) { + if (grid_get_line(gd, y)->flags & GRID_LINE_OUTPUT_COLLAPSED) + return (1); + } + return (0); +} + +/* Is the output following this prompt collapsed? */ +static int +window_copy_prompt_collapsed(struct screen *s, u_int prompt) +{ + struct grid *gd = s->grid; + struct grid_line *gl; + u_int y; + + for (y = prompt; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (y != prompt && gl->flags & GRID_LINE_START_PROMPT) + break; + if (gl->flags & GRID_LINE_START_OUTPUT) + return (gl->flags & GRID_LINE_OUTPUT_COLLAPSED); + } + return (0); +} + +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 copy-mode backing screen from the source grid. When output is + * collapsed, omitted cells and their line endings are not written, so the + * usual copy-mode operations work on the visible text only. + */ +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, *prompt = NULL; + struct screen_write_ctx ctx; + struct grid_cell gc; + u_int y, x, total, dy, output_line = UINT_MAX; + int hiding = 0, hide_prompt = 0; + + 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); + } + 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; + + screen_write_start(&ctx, dst); + for (y = 0; y < total; y++) { + sgl = grid_get_line(sgd, y); + if (sgl->flags & GRID_LINE_START_PROMPT) { + hide_prompt = (options_get_number(wme->wp->window->options, + "collapse-output") != 0 && + window_copy_prompt_collapsed(src, y)); + prompt = NULL; + } + for (x = 0; x <= sgl->cellused; x++) { + if ((sgl->flags & GRID_LINE_START_PROMPT) && x == sgl->prompt) { + dy = dst->grid->hsize + dst->cy; + dgl = grid_get_line(dst->grid, dy); + dgl->flags |= GRID_LINE_START_PROMPT; + dgl->source_line = y; + prompt = dgl; + } + if ((sgl->flags & GRID_LINE_START_COMMAND) && x == sgl->command) { + dy = dst->grid->hsize + dst->cy; + dgl = grid_get_line(dst->grid, dy); + dgl->flags |= GRID_LINE_START_COMMAND; + dgl->command = dst->cx; + hide_prompt = 0; + } + if ((sgl->flags & GRID_LINE_START_OUTPUT) && x == sgl->output) { + dy = dst->grid->hsize + dst->cy; + dgl = grid_get_line(dst->grid, dy); + dgl->flags |= GRID_LINE_START_OUTPUT; + dgl->output = dst->cx; + output_line = y; + hiding = (sgl->flags & GRID_LINE_OUTPUT_COLLAPSED) != 0; + if (prompt != NULL) { + prompt->flags |= GRID_LINE_OUTPUT_CONTROL; + prompt->output_line = output_line; + if (hiding) + prompt->flags |= GRID_LINE_OUTPUT_COLLAPSED; + } + } + if ((sgl->flags & GRID_LINE_END_OUTPUT) && x == sgl->end_output) { + dy = dst->grid->hsize + dst->cy; + dgl = grid_get_line(dst->grid, dy); + dgl->flags |= GRID_LINE_END_OUTPUT; + dgl->end_output = dst->cx; + hiding = 0; + output_line = UINT_MAX; + } + if (x == sgl->cellused) + break; + if (hiding || hide_prompt) + continue; + dy = dst->grid->hsize + dst->cy; + dgl = grid_get_line(dst->grid, dy); + dgl->source_line = y; + if (output_line != UINT_MAX) { + dgl->flags |= GRID_LINE_OUTPUT_MEMBER; + dgl->output_line = output_line; + } + grid_get_cell(sgd, x, y, &gc); + if (gc.flags & GRID_FLAG_PADDING) + continue; + screen_write_cell(&ctx, &gc); + } + if (y + 1 != total && !(sgl->flags & GRID_LINE_WRAPPED) && + !hiding) { + screen_write_carriagereturn(&ctx); + screen_write_linefeed(&ctx, 0, 8); + } + } + 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,7 +643,7 @@ 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; @@ -486,7 +656,7 @@ window_copy_sync_backing(struct window_mode_entry *wme) * 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. */ @@ -607,8 +777,21 @@ window_copy_init(struct window_mode_entry *wme, u_int i, cx, cy; 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->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')) { + for (i = 0; i < data->source->grid->hsize + + data->source->grid->sy; i++) { + if (grid_get_line(data->source->grid, i)->flags & + GRID_LINE_START_OUTPUT) + grid_get_line(data->source->grid, i)->flags |= + GRID_LINE_OUTPUT_COLLAPSED; + } + } + window_copy_rebuild_backing(wme); window_copy_sync_snapshot(data, base->grid); data->cx = cx; @@ -622,6 +805,13 @@ window_copy_init(struct window_mode_entry *wme, data->scroll_exit = args_has(args, 'e'); data->hide_position = args_has(args, 'H'); + if (data->oy > screen_hsize(data->backing)) + data->oy = screen_hsize(data->backing); + if (args_has(args, 'c')) { + data->cx = 0; + data->cy = screen_size_y(&data->screen) - 1; + data->oy = 0; + } if (base->hyperlinks != NULL) { hyperlinks_free(data->screen.hyperlinks); @@ -685,6 +875,10 @@ 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); @@ -1214,6 +1408,15 @@ window_copy_resize(struct window_mode_entry *wme, u_int sx, u_int sy) int reflow; screen_resize(s, sx, sy, 0); + if (data->source != NULL) { + window_copy_rebuild_backing(wme); + data->cx = 0; + data->cy = screen_size_y(s) - 1; + data->oy = 0; + window_copy_size_changed(wme); + window_copy_redraw_screen(wme); + return; + } cx = data->cx; if (data->oy > gd->hsize + data->cy) data->oy = gd->hsize + data->cy; @@ -2998,11 +3201,14 @@ 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, + if (data->source != NULL) { + screen_free(data->source); + free(data->source); + } + data->source = window_copy_clone_screen(&wp->base, &data->screen, NULL, NULL, wme->swp != wme->wp); } + window_copy_rebuild_backing(wme); if (follow) { data->cy = screen_size_y(&data->screen) - 1; @@ -3190,6 +3396,166 @@ 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 *prompt, + u_int *chosen) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->source->grid; + struct grid_line *gl; + u_int target, last_prompt = 0, output = UINT_MAX, y; + + *prompt = 0; + *chosen = UINT_MAX; + gl = grid_get_line(data->backing->grid, + data->backing->grid->hsize + data->cy - data->oy); + target = gl->source_line; + if (gl->flags & (GRID_LINE_OUTPUT_CONTROL|GRID_LINE_OUTPUT_MEMBER)) { + *chosen = gl->output_line; + for (y = 0; y <= *chosen; y++) { + if (grid_get_line(gd, y)->flags & GRID_LINE_START_PROMPT) + *prompt = y; + } + return (1); + } + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (gl->flags & GRID_LINE_START_PROMPT) + last_prompt = y; + if (gl->flags & GRID_LINE_START_OUTPUT) { + output = y; + *prompt = last_prompt; + } + if (gl->flags & GRID_LINE_END_OUTPUT && output != UINT_MAX) { + if (target >= *prompt && target <= y) { + *chosen = output; + return (1); + } + output = UINT_MAX; + } + } + if (output != UINT_MAX && target >= *prompt) { + *chosen = output; + return (1); + } + return (0); +} + +static int +window_copy_set_output(struct window_mode_entry *wme, int all, int collapse) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd; + struct grid_line *gl; + u_int prompt = 0, y, chosen = UINT_MAX; + int changed = 0, enabled = 0, wanted; + + if (data->source == NULL) + return (0); + if (!data->output_controls) { + data->output_controls = 1; + window_copy_rebuild_backing(wme); + enabled = 1; + } + gd = data->source->grid; + window_copy_find_output(wme, &prompt, &chosen); + if (all) { + wanted = collapse; + if (wanted == -1) + wanted = !window_copy_any_output_collapsed(data->source); + for (y = 0; y < gd->hsize + gd->sy; y++) { + gl = grid_get_line(gd, y); + if (!(gl->flags & GRID_LINE_START_OUTPUT)) + continue; + if (!!(gl->flags & GRID_LINE_OUTPUT_COLLAPSED) == wanted) + continue; + if (wanted) + gl->flags |= GRID_LINE_OUTPUT_COLLAPSED; + else + gl->flags &= ~GRID_LINE_OUTPUT_COLLAPSED; + changed = 1; + } + } else { + if (chosen == UINT_MAX) + return (0); + gl = grid_get_line(gd, chosen); + wanted = collapse; + if (wanted == -1) + wanted = !(gl->flags & GRID_LINE_OUTPUT_COLLAPSED); + if (!!(gl->flags & GRID_LINE_OUTPUT_COLLAPSED) != wanted) { + if (wanted) + gl->flags |= GRID_LINE_OUTPUT_COLLAPSED; + else + gl->flags &= ~GRID_LINE_OUTPUT_COLLAPSED; + 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 (chosen != UINT_MAX) { + for (y = 0; y < data->backing->grid->hsize + + data->backing->grid->sy; y++) { + gl = grid_get_line(data->backing->grid, y); + if (gl->source_line != prompt || + !(gl->flags & GRID_LINE_OUTPUT_CONTROL)) + continue; + data->cx = gl->command; + if (y < data->backing->grid->hsize) { + data->cy = 0; + data->oy = data->backing->grid->hsize - y; + } else { + data->cy = y - data->backing->grid->hsize; + data->oy = 0; + } + 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)) + return (WINDOW_COPY_CMD_REDRAW); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_uncollapse_output(struct window_copy_cmd_state *cs) +{ + if (window_copy_set_output(cs->wme, args_has(cs->wargs, 'a'), 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) +{ + u_int x, y; + + 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); + } + if (window_copy_set_output(cs->wme, args_has(cs->wargs, 'a'), -1)) + return (WINDOW_COPY_CMD_REDRAW); + return (WINDOW_COPY_CMD_NOTHING); +} + static const struct { const char *command; u_int minargs; @@ -3472,6 +3838,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 = "uncollapse-output", + .args = { "a", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_uncollapse_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, @@ -5190,10 +5574,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) @@ -5210,7 +5603,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) @@ -5287,16 +5680,19 @@ window_copy_write_line(struct window_mode_entry *wme, struct screen *s = &data->screen; struct options *oo = wp->window->options; struct grid_cell gc, mgc, cgc, mkgc, ln_gc, cur_ln_gc; + struct grid_line *gl; u_int sx = screen_size_x(s); u_int hsize = screen_hsize(data->backing); - u_int width; + u_int width, line_width, gutter_width; 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 = gutter_width + line_width; if (width >= sx) content_sx = 1; else if (width != 0) @@ -5323,7 +5719,17 @@ 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; + gl = grid_get_line(data->backing->grid, hsize - data->oy + py); + absolute = gl->source_line + 1; + if (gutter_width != 0) { + screen_write_cursormove(ctx, 0, py, 0); + screen_write_nputs(ctx, gutter_width, + current ? &cur_ln_gc : &ln_gc, " %c ", + (gl->flags & GRID_LINE_OUTPUT_COLLAPSED) ? '>' : + (gl->flags & GRID_LINE_OUTPUT_CONTROL) ? 'V' : ' '); + } + if (line_width == 0) + goto write_content; mode = window_copy_line_number_mode(wme); if (mode == WINDOW_COPY_LINE_NUMBERS_DEFAULT) { if (py < data->oy) @@ -5338,11 +5744,12 @@ 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); + screen_write_cursormove(ctx, gutter_width, py, 0); + screen_write_nputs(ctx, line_width, current ? &cur_ln_gc : &ln_gc, + "%*u ", (int)line_width - 1, line_number); } +write_content: window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, content_sx, &mgc, &cgc, &mkgc); @@ -5415,7 +5822,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); @@ -5806,10 +6213,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)); }