From 0da1c84c5330b7bdadf46511a72ce979ee22dd30 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 29 Sep 2026 16:42:21 +0100 Subject: [PATCH] Add collapsible OSC 133 output to copy mode --- cmd-copy-mode.c | 4 +- grid.c | 2 + input.c | 63 +- key-bindings.c | 13 +- options-table.c | 8 +- regress/collapse-output.sh | 84 ++ regress/copy-mode-exit-status.sh | 102 +++ regress/input-modes.sh | 16 +- regress/output-commands.sh | 21 +- tmux.1 | 44 +- tmux.h | 4 +- window-copy.c | 1243 +++++++++++++++++++++++++++--- 12 files changed, 1485 insertions(+), 119 deletions(-) create mode 100644 regress/collapse-output.sh create mode 100644 regress/copy-mode-exit-status.sh diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index 28a1a742e..59baed830 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 424f5bea3..8eb9bf8cd 100644 --- a/grid.c +++ b/grid.c @@ -1760,6 +1760,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 df53c947d..c3707c59c 100644 --- a/input.c +++ b/input.c @@ -3192,13 +3192,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; + char *endptr; long long status; + *present = 0; if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2) return (0); end = strchr(p + 2, ';'); @@ -3212,9 +3213,11 @@ input_osc_133_exit_status(const char *p) free(copy); return (0); } - status = strtonum(copy, 0, 255, &errstr); + *present = 1; + errno = 0; + status = (int)strtol(copy, &endptr, 10); free(copy); - if (errstr != NULL) + if (errno != 0 || endptr == copy || status < 0 || status > 255) return (255); return (status); } @@ -3259,6 +3262,22 @@ input_fire_command_event(struct window_pane *wp, const char *name) events_fire(name, ep); } +/* Check if an OSC 133 prompt is secondary or a continuation. */ +static int +input_osc_133_secondary_prompt(const char *p) +{ + const char *cp; + + while ((cp = strstr(p, ";k=")) != NULL) { + cp += 3; + if ((*cp == 's' || *cp == 'c') && + (cp[1] == '\0' || cp[1] == ';')) + return (1); + p = cp; + } + return (0); +} + /* Handle the OSC 133 sequence. */ static void input_osc_133(struct input_ctx *ictx, const char *p) @@ -3268,8 +3287,7 @@ input_osc_133(struct input_ctx *ictx, const char *p) struct grid *gd = s->grid; 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); @@ -3278,9 +3296,14 @@ input_osc_133(struct input_ctx *ictx, const char *p) case 'A': case 'N': if (gl != NULL) { - gl->osc133_data.prompt_col = s->cx; - gl->osc133_data.cmd_col = 0; - 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 (input_osc_133_secondary_prompt(p)) + gl->flags |= GRID_LINE_SECOND_PROMPT; + else + gl->flags |= GRID_LINE_START_PROMPT; + } } if (wp != NULL) { wp->last_prompt_time = time(NULL); @@ -3289,23 +3312,25 @@ input_osc_133(struct input_ctx *ictx, const char *p) 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; + if (!(gl->flags & (GRID_LINE_START_PROMPT| + GRID_LINE_SECOND_PROMPT))) { + gl->osc133_data.prompt_col = s->cx; + if (input_osc_133_secondary_prompt(p)) + 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; } @@ -3318,7 +3343,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; @@ -3327,6 +3352,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/key-bindings.c b/key-bindings.c index de288d02c..f2bb7fc6a 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -52,7 +52,8 @@ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ " ''" \ - " '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Line Numbers Off,Line Numbers On},}' 'L' {send -X line-numbers-toggle}" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?copy_fold_view,Fold View Off,Fold View On},}' 'O' {send -X fold-view-toggle}" \ " '#{?#{==:#{pane_mode},copy-mode},Edit,}' 'e' {if -F '#{selection_present}' {send -X open-selection} {send -X open-output}}" \ " '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \ " ''" \ @@ -585,11 +586,14 @@ 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 -- '%%' } }", "bind -Tcopy-mode L { send -X line-numbers-toggle }", "bind -Tcopy-mode N { send -X search-reverse }", + "bind -Tcopy-mode O { send -X fold-view-toggle }", "bind -Tcopy-mode P { send -X toggle-position }", "bind -Tcopy-mode R { send -X rectangle-toggle }", "bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", @@ -603,7 +607,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 }", @@ -663,6 +667,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 -- '%%' } }", @@ -693,6 +699,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi M-e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }", "bind -Tcopy-mode-vi M-o { send -X select-output }", "bind -Tcopy-mode-vi N { send -X search-reverse }", + "bind -Tcopy-mode-vi O { send -X fold-view-toggle }", "bind -Tcopy-mode-vi P { send -X toggle-position }", "bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", "bind -Tcopy-mode-vi V { send -X select-line }", @@ -720,7 +727,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 e52e99f7e..eed829452 100644 --- a/options-table.c +++ b/options-table.c @@ -115,7 +115,6 @@ static const char *options_table_theme_list[] = { static const char *options_table_copy_mode_line_numbers_list[] = { "off", "default", "absolute", "relative", "hybrid", NULL }; - /* Status line format. */ #define OPTIONS_TABLE_STATUS_FORMAT1 \ "#[align=left range=left #{E:status-left-style}]" \ @@ -1385,6 +1384,13 @@ const struct options_table_entry options_table[] = { .text = "Style of search matches in copy mode." }, + { .name = "copy-mode-exit-status-format", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, + .default_str = "#{?exit_status,#[fg=themered]!, }", + .text = "Format of OSC 133 exit status indicator in copy mode." + }, + { .name = "copy-mode-current-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..63faa0334 --- /dev/null +++ b/regress/collapse-output.sh @@ -0,0 +1,84 @@ +#!/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\\n\\033]133;C\\007hello\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\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 -X search-backward separator || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != separator ] || exit 1 + +$TMUX send-keys BTab || 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 kill-server +$TMUX new-session -d -x80 -y20 "sh -c 'printf \"first\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007\"; exec sleep 100'" || exit 1 +sleep 1 + +$TMUX copy-mode -c || exit 1 +$TMUX send-keys -X search-backward separator || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != separator ] || exit 1 +$TMUX send-keys BTab || exit 1 +$TMUX send-keys -X search-backward separator || exit 1 +shown=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$shown" = separator ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + +$TMUX kill-server +$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P> \\033]133;B\\007for i in 1 2 3; do\\n\\033]133;A;k=s\\007> \\033]133;B\\007echo \\\$i\\n\\033]133;P;k=s\\007> \\033]133;B\\007done\\n\\033]133;C\\007RESULT\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007P> \\033]133;B\\007\"; exec sleep 100'" || exit 1 +sleep 1 + +$TMUX copy-mode -c || exit 1 +$TMUX send-keys -X search-backward RESULT || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != RESULT ] || exit 1 +$TMUX send-keys -X search-backward-text 'echo $i' || exit 1 +shown=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$shown" = '> echo $i' ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + +$TMUX copy-mode -U || exit 1 +$TMUX send-keys -X search-backward RESULT || exit 1 +$TMUX send-keys Tab || exit 1 +cursor=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$cursor" = '> done' ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + +$TMUX kill-server +$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P0> \\033]133;B\\007\\r\\n\\033]133;D;0\\007empty-separator\\n\\033]133;A\\007P1> \\033]133;B\\007\"; exec sleep 100'" || exit 1 +sleep 1 + +$TMUX copy-mode -c || exit 1 +collapsed=$($TMUX capture-pane -Mp) +case "$collapsed" in +*empty-separator*) exit 1 ;; +*P0\>*P1\>*) ;; +*) exit 1 ;; +esac +cursor=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$cursor" = 'P1>' ] || exit 1 +$TMUX send-keys -X search-backward empty-separator || exit 1 +hidden=$($TMUX display-message -p '#{copy_cursor_line}') +[ "$hidden" != empty-separator ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/copy-mode-exit-status.sh b/regress/copy-mode-exit-status.sh new file mode 100644 index 000000000..76bb0ec6c --- /dev/null +++ b/regress/copy-mode-exit-status.sh @@ -0,0 +1,102 @@ +#!/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\007silent\n\033]133;C\007\033]133;D;7\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 +$TMUX2 list-keys -Tcopy-mode O | grep -Fq 'fold-view-toggle' || exit 1 +$TMUX2 list-keys -Tcopy-mode-vi O | grep -Fq 'fold-view-toggle' || 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 "! p\$ silent" +check_no_grep "!+ p\$ silent" +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" +check_grep "! p\$ silent" +check_no_grep "!+ p\$ silent" +$TMUX2 send -X line-numbers-on || exit 1 +sleep 1 +capture +check_grep " 3 !+ p\$ two" +$TMUX2 send -X fold-view-toggle || exit 1 +[ "$($TMUX2 display -p '#{copy_fold_view}')" = 0 ] || exit 1 +sleep 1 +capture +check_grep "out2" +check_no_grep "!+ p\$ two" +check_grep " 3 p\$ two" +$TMUX2 send -X fold-view-toggle || exit 1 +[ "$($TMUX2 display -p '#{copy_fold_view}')" = 1 ] || exit 1 +sleep 1 +capture +check_grep " 3 !- 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 diff --git a/regress/input-modes.sh b/regress/input-modes.sh index 8ae905f89..cf0e5f875 100644 --- a/regress/input-modes.sh +++ b/regress/input-modes.sh @@ -5,9 +5,12 @@ start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n' check_capture alternate 'MAINZ' -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' +start_pane osc133 20 12 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nxz\033]133;A;k=s\007more\nxw\033]133;P;k=c\007more\nxv\033]133;A;k=c\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 +xzmore +xwmore +xvmore zzout qbad qqbig @@ -15,10 +18,13 @@ 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' + 'L 2 \(2\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \ + 'L 3 \(3\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \ + 'L 4 \(4\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \ + 'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,7' \ + 'L 6 \(6\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,1,4,255' \ + 'L 7 \(7\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,255' \ + 'L 8 \(8\) 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/regress/output-commands.sh b/regress/output-commands.sh index d686d0607..323660b05 100755 --- a/regress/output-commands.sh +++ b/regress/output-commands.sh @@ -53,6 +53,13 @@ $TMUX send-keys -X copy-output || exit 1 [ "$($TMUX show-buffer)" = "$EXPECTED" ] || exit 1 $TMUX send-keys -X cancel || exit 1 +$TMUX copy-mode -c || exit 1 +$TMUX send-keys -X search-backward echo || exit 1 +$TMUX send-keys -X select-output || exit 1 +$TMUX send-keys -X copy-selection || exit 1 +[ "$($TMUX show-buffer)" = "$EXPECTED" ] || exit 1 +$TMUX send-keys -X cancel || exit 1 + $TMUX copy-mode || exit 1 $TMUX send-keys -X search-backward one || exit 1 $TMUX set-buffer sentinel || exit 1 @@ -79,7 +86,7 @@ $TMUX send-keys -t :plain.0 -X copy-output || exit 1 [ "$($TMUX show-buffer -b keep)" = unchanged ] || exit 1 $TMUX send-keys -t :plain.0 -X cancel || exit 1 -$TMUX new-window -d -n empty "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007true\\n\\033]133;C\\007\\033]133;D;0\\007'; exec sleep 100" || exit 1 +$TMUX new-window -d -n empty "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007true\\n\\033]133;C\\007\\033]133;D;0\\007'; exec sleep 100" || exit 1 sleep 1 $TMUX copy-mode -t :empty || exit 1 $TMUX set-buffer sentinel || exit 1 @@ -95,6 +102,15 @@ $TMUX send-keys -t :prompt.0 -X copy-output || exit 1 [ "$($TMUX show-buffer)" = one ] || exit 1 $TMUX send-keys -t :prompt.0 -X cancel || exit 1 +$TMUX copy-mode -c -t :prompt || exit 1 +$TMUX send-keys -t :prompt.0 -X expand-output || exit 1 +$TMUX send-keys -t :prompt.0 -X -N 100 cursor-down || exit 1 +$TMUX send-keys -t :prompt.0 -X select-output || exit 1 +[ "$($TMUX display-message -p -t :prompt.0 '#{selection_present}')" = 1 ] || exit 1 +$TMUX send-keys -t :prompt.0 -X copy-selection || exit 1 +[ "$($TMUX show-buffer)" = one ] || exit 1 +$TMUX send-keys -t :prompt.0 -X cancel || exit 1 + $TMUX copy-mode -t :prompt || exit 1 $TMUX send-keys -t :prompt.0 -X -N 100 cursor-down || exit 1 $TMUX send-keys -t :prompt.0 C-o || exit 1 @@ -114,13 +130,14 @@ $TMUX send-keys -t :same.0 -X copy-selection || exit 1 [ "$($TMUX show-buffer)" = one ] || exit 1 $TMUX send-keys -t :same.0 -X cancel || exit 1 +$TMUX set-option -g scroll-on-clear off || exit 1 $TMUX new-window -d -n clear "printf 'old1\\nold2\\nold3\\nold4\\nold5\\nold6\\n\\033]133;A\\007p\\$ \\033]133;B\\007echo 1; clear; ps\\n\\033]133;C\\0071\\n\\033[H\\033[2JPID TTY\\n1 pts/0\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1 sleep 1 $TMUX copy-mode -t :clear || exit 1 $TMUX send-keys -t :clear.0 C-o || exit 1 $TMUX set-buffer sentinel || exit 1 $TMUX send-keys -t :clear.0 -X copy-selection || exit 1 -[ "$($TMUX show-buffer)" = "$(printf '1\nPID TTY\n1 pts/0')" ] || exit 1 +[ "$($TMUX show-buffer)" = "$(printf 'PID TTY\n1 pts/0')" ] || exit 1 $TMUX send-keys -t :clear.0 -X cancel || exit 1 $TMUX new-window -d -n oneline "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\033]133;D;0\\007\\nseparator\\n\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1 diff --git a/tmux.1 b/tmux.1 index 14b40d7ef..decd2cabf 100644 --- a/tmux.1 +++ b/tmux.1 @@ -2196,6 +2196,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) @@ -2692,12 +2713,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 @@ -5907,6 +5935,20 @@ 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 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. +.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 22a8c35b5..fe9997eed 100644 --- a/tmux.h +++ b/tmux.h @@ -819,6 +819,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 \ @@ -826,7 +827,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 084d531f3..0e1b6408f 100644 --- a/window-copy.c +++ b/window-copy.c @@ -28,6 +28,13 @@ struct window_copy_mode_data; +#define WINDOW_COPY_LINE_CONTROL 0x1 +#define WINDOW_COPY_LINE_MEMBER 0x2 +#define WINDOW_COPY_LINE_INITIAL 0x4 +#define WINDOW_COPY_LINE_EXPANDABLE 0x8 + +#define WINDOW_COPY_FOLD_COLLAPSED 0x1 + 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 *, @@ -65,6 +72,7 @@ static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); static int window_copy_cursor_line_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 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, @@ -73,6 +81,20 @@ 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_fold(struct window_mode_entry *, int, int, + u_int, int); +static int window_copy_find_fold(struct window_mode_entry *, u_int, + u_int *, u_int *); +static void window_copy_cursor_source(struct window_mode_entry *, u_int *, + u_int *); +static int window_copy_source_position(struct window_mode_entry *, u_int, + 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); @@ -119,13 +141,13 @@ static int window_copy_update_selection(struct window_mode_entry *, int, static void window_copy_synchronize_cursor(struct window_mode_entry *, int); static int window_copy_find_output_range(struct window_mode_entry *, u_int *, u_int *, u_int *, u_int *); -static int window_copy_find_previous_output_range(struct window_mode_entry *, +static int window_copy_find_previous_output_range(struct screen *, u_int, u_int, u_int *, u_int *, u_int *, u_int *); static void window_copy_output_end(struct screen *, u_int *, u_int *); static void *window_copy_get_selection(struct window_mode_entry *, size_t *); static void *window_copy_get_output(struct window_mode_entry *, size_t *, int); -static void *window_copy_get_grid_range(struct window_mode_entry *, u_int, +static void *window_copy_get_grid_range(struct grid *, u_int, u_int, u_int, u_int, size_t *); static void window_copy_copy_buffer(struct window_mode_entry *, const char *, void *, size_t, int, int); @@ -142,6 +164,8 @@ static void window_copy_append_selection(struct window_mode_entry *); static void window_copy_clear_selection(struct window_mode_entry *); static void window_copy_copy_line(struct window_mode_entry *, char **, size_t *, u_int, u_int, u_int); +static void window_copy_copy_grid_line(struct grid *, char **, size_t *, + u_int, u_int, u_int); static int window_copy_in_set(struct window_mode_entry *, u_int, u_int, const char *); static u_int window_copy_find_length(struct window_mode_entry *, u_int); @@ -261,6 +285,14 @@ struct window_copy_cmd_state { struct winlink *wl; }; +struct window_copy_line { + u_int source_line; + u_int output_line; + u_char flags; + u_char exit_status; + u_char exit_status_present; +}; + /* * 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 @@ -279,8 +311,13 @@ struct window_copy_mode_data { struct screen screen; struct screen *backing; - int backing_written; /* backing display started */ - struct input_ctx *ictx; + struct screen *source; + u_char *source_flags; /* Flags per source line */ + u_int source_line_count; + struct window_copy_line *lines; + u_int line_count; + int backing_written; /* backing display started */ + struct input_ctx *ictx; u_int sync_added; /* snapshot of backing grid counters */ u_int sync_collected; @@ -311,7 +348,9 @@ struct window_copy_mode_data { int rectflag; /* in rectangle copy mode? */ 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 line_numbers; /* 0 off, 1 from option, 2 absolute */ + int fold_view; + u_int output_status_width; enum { SEL_CHAR, /* select one char at a time */ @@ -459,6 +498,468 @@ 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, int expandable) +{ + 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; + if (expandable) + line->flags |= WINDOW_COPY_LINE_EXPANDABLE; +} + +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 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->fold_view || 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_set_source_folded(struct window_copy_mode_data *data, u_int y, + int folded) +{ + if (!!(data->source_flags[y] & WINDOW_COPY_FOLD_COLLAPSED) == folded) + return (0); + if (folded) + data->source_flags[y] |= WINDOW_COPY_FOLD_COLLAPSED; + else + data->source_flags[y] &= ~WINDOW_COPY_FOLD_COLLAPSED; + return (1); +} + +static int +window_copy_any_fold_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->source_flags[0] & WINDOW_COPY_FOLD_COLLAPSED)) + /* Output which has scrolled off top of buffer is collapsed. */ + 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->source_flags[y] & WINDOW_COPY_FOLD_COLLAPSED) + return (1); + output = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (!output && command && + (data->source_flags[y] & WINDOW_COPY_FOLD_COLLAPSED)) + return (1); + command = output = 0; + } + } + return (0); +} + +static int +window_copy_set_all_folds(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 && + window_copy_set_source_folded(data, 0, 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 (window_copy_set_source_folded(data, y, collapse)) { + changed = 1; + } + output = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (!output && command && + window_copy_set_source_folded(data, y, collapse)) { + changed = 1; + } + command = output = 0; + } + } + return (changed); +} + +/* 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 window_copy_line *line; + 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, tree = 0; + int in_output = 0, output_empty = 0; + int initial = 0, was_hiding; + + if (src == NULL) + return; + if (!data->fold_view) { + 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_status_width = 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].source_line = UINT_MAX; + 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) + /* + * The source begins in an existing command or its output. Treat its + * first line as a synthetic command for the region before the first A. + */ + initial = 1; + + screen_write_start(&ctx, dst); + /* + * Markers may occur at any column, so copy the source one cell boundary at + * a time. The final boundary (cellused) is included to handle a marker + * immediately after the last cell on a line. + */ + 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) + /* + * Row is visible. Records the source + * row and current output-block identity + * for cursor restoration, selection, + * gutter drawing, and line numbers. + */ + window_copy_set_line(data, dy, y, output_line, 0); + /* + * Continue even if row is folded because + * there may be markers to process. + */ + dgl = grid_get_line(dst->grid, dy); + if (initial && y == 0 && x == 0) + /* Top line of buffer, no osc133 marker yet. + * Attach controls to top line, make it a + * "synthetic command" so top lines can fold. + */ + window_copy_set_line(data, dy, y, UINT_MAX, + WINDOW_COPY_LINE_CONTROL | WINDOW_COPY_LINE_INITIAL); + if ((sgl->flags & GRID_LINE_START_OUTPUT) && + x == osc133->out_start_col) { + /* + * osc133 C starts the collapsible region + * controlled by this source line. + */ + dgl->flags |= GRID_LINE_START_OUTPUT; + dgl->osc133_data.out_start_col = dst->cx; + output_line = y; + tree = 1; /* used to draw the | or L in gutter */ + in_output = 1; + output_empty = 1; + collapsed = data->source_flags[y] & + WINDOW_COPY_FOLD_COLLAPSED; + hiding = collapsed; /* Start/stop hiding cells */ + if (prompt_dy != UINT_MAX) + /* Attach +/- controls to the prompt. */ + window_copy_set_control(data, prompt_dy, + output_line, 1); + } + if ((sgl->flags & GRID_LINE_END_OUTPUT) && + x == osc133->out_end_col) { + /* + * Command output ends at the osc133 D + * marker. There may be more lines + * (like pre-prompt output) before next A + * marker. Fold these with previous output + * until the following A marker. + */ + 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 (in_output && output_empty && prompt_dy != UINT_MAX) { + line = window_copy_get_line_info(data, prompt_dy); + if (line != NULL) + /* No output to hide, no +/-. */ + line->flags &= ~WINDOW_COPY_LINE_EXPANDABLE; + } + in_output = 0; + if (output_line == UINT_MAX && command) { + /* + * Saw osc133 B but not C marker. + * This could be user pressed enter + * or command like 'true' with no + * output. + */ + output_line = y; + tree = 1; + /* Block is collapsed? */ + collapsed = data->source_flags[y] & + WINDOW_COPY_FOLD_COLLAPSED; + if (prompt_dy != UINT_MAX) + /* + * Make this a +/- control row + * but without the +/- for now + * since we don't know what + * comes below. + */ + window_copy_set_control(data, prompt_dy, + output_line, 0); + } + if (prompt_dy != UINT_MAX && + sgl->flags & GRID_LINE_END_OUTPUT_STATUS) + /* + * osc133 D, record exit status on the + * command's prompt row. + */ + window_copy_set_exit_status(data, prompt_dy, + osc133->exit_status); + if (initial && prompt_dy == UINT_MAX) + /* At top of buffer before first osc A + * marker use first line's fold state. + */ + hiding = data->source_flags[0] & + WINDOW_COPY_FOLD_COLLAPSED; + else + /* + * Otherwise, use the current block's + * fold state. + */ + hiding = collapsed; + command = 0; + } + /* + * Process osc133 C and D before a following prompt + * at the same column. + */ + if ((sgl->flags & GRID_LINE_START_PROMPT) && + x == osc133->prompt_col) { + /* osc133 A ends region, starts new prompt. */ + dgl->flags |= GRID_LINE_START_PROMPT; + dgl->osc133_data.prompt_col = dst->cx; + prompt_dy = dy; + output_line = UINT_MAX; + collapsed = command = hiding = tree = 0; + in_output = output_empty = 0; + } + if ((sgl->flags & GRID_LINE_SECOND_PROMPT) && + x == osc133->prompt_col) { + /* osc133 A;k=s, marks secondary prompt. */ + dgl->flags |= GRID_LINE_SECOND_PROMPT; + dgl->osc133_data.prompt_col = dst->cx; + } + if ((sgl->flags & GRID_LINE_START_COMMAND) && + x == osc133->cmd_col) { + /* osc133 B marks start/continue of command. */ + dgl->flags |= GRID_LINE_START_COMMAND; + dgl->osc133_data.cmd_col = dst->cx; + command = 1; + } + /* + * An osc133 A may expose the remainder of this source + * line after a folded region. + */ + if (was_hiding && !hiding) + window_copy_set_line(data, dy, y, output_line, 0); + if (x == sgl->cellused) + break; + grid_get_cell(sgd, x, y, &gc); + if (in_output && !(gc.flags & GRID_FLAG_PADDING)) + output_empty = 0; + /* Separator text after D is also expandable. */ + if (!in_output && tree && output_line != UINT_MAX && + !(gc.flags & GRID_FLAG_PADDING) && prompt_dy != UINT_MAX) + window_copy_set_control(data, prompt_dy, output_line, 1); + /* + * osc133 marker processing continues while folded, but + * cell output does not. + */ + if (hiding) + continue; + 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)) + /* Mark empty visible lines so gutter tree continues */ + 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))) { + /* + * Hide folded line breaks, except after the initial + * synthetic command line (top line of copy buffer). + */ + screen_write_carriagereturn(&ctx); + screen_write_linefeed(&ctx, 0, 8); + } + if (initial && y == 0) { + /* + * Top line of buffer (which may not be a command) + * becomes our "synthisied" commmand to anchor lines + * folded below it up to first osc133 A. + */ + tree = 1; + hiding = data->source_flags[0] & + WINDOW_COPY_FOLD_COLLAPSED; + } + } + screen_write_stop(&ctx); + if (data->backing != NULL) { + screen_free(data->backing); + free(data->backing); + } + data->backing = dst; + window_copy_update_exit_status_width(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. @@ -486,20 +987,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. */ @@ -521,6 +1022,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->source_flags, data->source_flags + collected, + (old_count - collected) * sizeof *data->source_flags); + } + data->source_flags = xreallocarray(data->source_flags, new_count, + sizeof *data->source_flags); + if (new_count > old_count - collected) { + memset(data->source_flags + old_count - collected, 0, + (new_count - (old_count - collected)) * + sizeof *data->source_flags); + } + data->source_line_count = new_count; if (added == 0 && collected == 0) { /* History is unchanged; only the viewport can have mutated. */ @@ -565,6 +1080,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); } @@ -617,18 +1133,34 @@ 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->source_flags = xcalloc(screen_hsize(data->source) + + screen_size_y(data->source), sizeof *data->source_flags); + data->source_line_count = screen_hsize(data->source) + + screen_size_y(data->source); + data->fold_view = args_has(args, 'c') || args_has(args, 'U'); + if (args_has(args, 'c')) + window_copy_set_all_folds(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->fold_view && + window_copy_find_fold(wme, cy, &prompt, &chosen)) { + restored = window_copy_restore_cursor(wme, cx, cy); + if (!restored) + restored = window_copy_restore_command_end(wme, prompt, chosen); + } else if (data->fold_view) + restored = window_copy_restore_cursor(wme, cx, cy); + 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; } @@ -698,8 +1230,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->source_flags); + free(data->lines); screen_free(&data->screen); free(data); @@ -1139,6 +1677,7 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) format_add(ft, "copy_position_limit", "%u", limit); format_add(ft, "copy_line_numbers", "%d", window_copy_line_numbers_active(wme)); + format_add(ft, "copy_fold_view", "%d", data->fold_view); format_add(ft, "refresh_active", "%d", data->refresh_active); format_add(ft, "rectangle_toggle", "%d", data->rectflag); @@ -1607,14 +2146,15 @@ window_copy_cmd_select_output(struct window_copy_cmd_state *cs) struct grid_reader gr; void *buf; size_t len; - u_int sx, sy, ex, ey, total; + u_int sx, sy, ex, ey, bsx, bsy, bex, bey; + u_int total; int all = args_has(cs->wargs, 'a'); if (all) { sx = sy = 0; - total = data->backing->grid->hsize + data->backing->grid->sy; + total = data->source->grid->hsize + data->source->grid->sy; ey = total - 1; - ex = grid_get_line(data->backing->grid, ey)->cellused; + ex = grid_get_line(data->source->grid, ey)->cellused; } else if (!window_copy_find_output_range(wme, &sx, &sy, &ex, &ey)) return (WINDOW_COPY_CMD_NOTHING); buf = window_copy_get_output(wme, &len, all); @@ -1622,19 +2162,29 @@ window_copy_cmd_select_output(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); free(buf); + if (all && data->fold_view) { + window_copy_set_all_folds(data, 0); + window_copy_rebuild_backing(wme); + } else if (data->fold_view && + data->source_flags[sy] & WINDOW_COPY_FOLD_COLLAPSED) + window_copy_set_fold(wme, 0, 0, sy, 0); + if (!window_copy_source_position(wme, sx, sy, &bsx, &bsy) || + !window_copy_source_position(wme, ex, ey, &bex, &bey)) + return (WINDOW_COPY_CMD_NOTHING); + window_copy_clear_selection(wme); data->lineflag = LINE_SEL_NONE; data->rectflag = 0; data->selflag = SEL_CHAR; - window_copy_scroll_to(wme, sx, sy, 1); + window_copy_scroll_to(wme, bsx, bsy, 1); window_copy_start_selection(wme); if (options_get_number(wme->wp->window->options, "mode-keys") == MODEKEY_VI) { - grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_start(&gr, data->backing->grid, bex, bey); grid_reader_cursor_left(&gr, 1); - grid_reader_get_cursor(&gr, &ex, &ey); + grid_reader_get_cursor(&gr, &bex, &bey); } - window_copy_scroll_to(wme, ex, ey, 1); + window_copy_scroll_to(wme, bex, bey, 1); return (WINDOW_COPY_CMD_REDRAW); } @@ -3276,10 +3826,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->source_flags); + data->source_flags = xcalloc(screen_hsize(data->source) + + screen_size_y(data->source), sizeof *data->source_flags); + data->source_line_count = screen_hsize(data->source) + + screen_size_y(data->source); + window_copy_rebuild_backing(wme); } if (follow) { @@ -3497,6 +4053,390 @@ window_copy_cmd_line_numbers_toggle(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } +static enum window_copy_cmd_action +window_copy_cmd_fold_view_toggle(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + u_int source_x, source_y, hsize; + + if (data->source == NULL) + return (WINDOW_COPY_CMD_NOTHING); + window_copy_cursor_source(wme, &source_x, &source_y); + memset(data->source_flags, 0, + data->source_line_count * sizeof *data->source_flags); + data->fold_view = !data->fold_view; + window_copy_clear_selection(wme); + window_copy_clear_marks(wme); + window_copy_rebuild_backing(wme); + if (window_copy_restore_cursor(wme, source_x, source_y)) + return (WINDOW_COPY_CMD_REDRAW); + + hsize = screen_hsize(data->backing); + if (source_y < hsize) { + data->cy = 0; + data->oy = hsize - source_y; + } else { + data->cy = source_y - hsize; + data->oy = 0; + } + return (WINDOW_COPY_CMD_REDRAW); +} + +/* Find the fold containing target. Return its prompt and source flag line. */ +static int +window_copy_find_fold(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 if (line->source_line == UINT_MAX) { + *source_x = UINT_MAX; + *source_y = data->source_line_count - 1; + } + else + *source_y = line->source_line; +} + +static int +window_copy_source_position(struct window_mode_entry *wme, u_int source_x, + u_int source_y, u_int *backing_x, u_int *backing_y) +{ + struct window_copy_mode_data *data = wme->data; + struct window_copy_line *line; + u_int y, total; + + if (!data->fold_view) { + *backing_x = source_x; + *backing_y = source_y; + return (1); + } + total = screen_hsize(data->backing) + screen_size_y(data->backing); + for (y = 0; y < total; y++) { + line = window_copy_get_line_info(data, y); + if (line == NULL || line->source_line != source_y) + continue; + *backing_x = source_x; + *backing_y = y; + return (1); + } + return (0); +} + +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_fold(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; + int 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->fold_view) { + data->fold_view = 1; + window_copy_rebuild_backing(wme); + enabled = 1; + } + found = window_copy_find_fold(wme, target, &prompt, &chosen); + if (all) { + wanted = collapse; + if (wanted == -1) + wanted = !window_copy_any_fold_collapsed(data); + changed = window_copy_set_all_folds(data, wanted); + } else if (initial) { + wanted = collapse; + if (wanted == -1) + wanted = !(data->source_flags[0] & + WINDOW_COPY_FOLD_COLLAPSED); + if (window_copy_set_source_folded(data, 0, wanted)) { + changed = 1; + } + } else if (found && chosen != UINT_MAX) { + wanted = collapse; + if (wanted == -1) + wanted = !(data->source_flags[chosen] & + WINDOW_COPY_FOLD_COLLAPSED); + if (window_copy_set_source_folded(data, chosen, wanted)) { + changed = 1; + } + } + if (!changed) { + if (enabled) { + data->fold_view = 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_fold(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_fold(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; + u_int target_prompt, chosen, x, y; + int command_end = 0, current_found; + int 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 || line->source_line == UINT_MAX) + 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_fold(cs->wme, current, + ¤t_prompt, &chosen); + target_found = initial || window_copy_find_fold(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_fold(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; @@ -3791,6 +4731,30 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_NEVER, .f = window_copy_cmd_line_numbers_toggle }, + { .command = "fold-view-toggle", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_fold_view_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, @@ -4155,15 +5119,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; @@ -5511,13 +6481,19 @@ window_copy_line_number_mode(struct window_mode_entry *wme) return (WINDOW_COPY_LINE_NUMBERS_OFF); mode = options_get_number(oo, "copy-mode-line-numbers"); if (data->line_numbers == 2 && mode == WINDOW_COPY_LINE_NUMBERS_OFF) - return (WINDOW_COPY_LINE_NUMBERS_DEFAULT); + return (WINDOW_COPY_LINE_NUMBERS_ABSOLUTE); return (mode); } static int window_copy_line_number_is_absolute(struct window_mode_entry *wme) { + struct window_copy_mode_data *data = wme->data; + + if (data->fold_view && + window_copy_line_number_mode(wme) == + WINDOW_COPY_LINE_NUMBERS_DEFAULT) + return (1); switch (window_copy_line_number_mode(wme)) { case WINDOW_COPY_LINE_NUMBERS_ABSOLUTE: case WINDOW_COPY_LINE_NUMBERS_RELATIVE: @@ -5567,10 +6543,22 @@ 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; + u_int width = 0; + + if (data->fold_view) + width = data->output_status_width + 2; + + return (window_copy_line_number_width(wme) + width); +} + 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) @@ -5587,7 +6575,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) @@ -5665,23 +6653,28 @@ window_copy_write_line(struct window_mode_entry *wme, struct options *oo = wp->window->options; struct grid_cell gc, mgc, cgc, mkgc, clgc, ln_gc; struct grid_cell cur_ln_gc; + struct grid_cell control_gc; + struct window_copy_line *line, *next; 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; + u_int i; const char *value; - char *expanded; + char *expanded, *status; struct format_tree *ft; + char control; int current, mode; - width = window_copy_line_number_width(wme); + line_width = window_copy_line_number_width(wme); + gutter_width = data->fold_view ? data->output_status_width + 2 : 0; + width = line_width + gutter_width; if (width >= sx) content_sx = 1; else if (width != 0) content_sx = sx - width; else content_sx = sx; - screen_write_cursormove(ctx, 0, py, 0); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -5703,9 +6696,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; + backing_y = hsize - data->oy + py; + absolute = backing_y + 1; + if (data->lines != NULL && backing_y < data->line_count && + data->lines[backing_y].source_line != UINT_MAX) + absolute = data->lines[backing_y].source_line + 1; + else if (data->fold_view && data->source_line_count != 0) + absolute = data->source_line_count; mode = window_copy_line_number_mode(wme); - if (mode == WINDOW_COPY_LINE_NUMBERS_DEFAULT) { + if (mode == WINDOW_COPY_LINE_NUMBERS_DEFAULT && data->fold_view) + line_number = absolute; + else if (mode == WINDOW_COPY_LINE_NUMBERS_DEFAULT) { if (py < data->oy) line_number = data->oy - py; else @@ -5718,9 +6719,64 @@ 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) { + line = NULL; + control = ' '; + 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->source_flags[0] & + WINDOW_COPY_FOLD_COLLAPSED ? '+' : '-'; + } else if (line->flags & WINDOW_COPY_LINE_EXPANDABLE && + line->output_line < data->source_line_count) { + if (data->source_flags[line->output_line] & + WINDOW_COPY_FOLD_COLLAPSED) + control = '+'; + else + 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); + for (i = 0; i < gutter_width; i++) + screen_write_putc(ctx, current ? &cur_ln_gc : &ln_gc, ' '); + 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); + } + 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, @@ -5732,8 +6788,7 @@ window_copy_write_line(struct window_mode_entry *wme, expanded = format_expand(ft, value); if (*expanded != '\0') { screen_write_cursormove(ctx, width, 0, 0); - format_draw(ctx, &gc, content_sx, expanded, - NULL, 0); + format_draw(ctx, &gc, content_sx, expanded, NULL, 0); } free(expanded); } @@ -5795,7 +6850,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); @@ -5836,6 +6891,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); } @@ -5952,8 +7008,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 || @@ -6191,10 +7247,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)); } @@ -6231,19 +7287,19 @@ window_copy_find_output_range(struct window_mode_entry *wme, u_int *sx, u_int *sy, u_int *ex, u_int *ey) { struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; + struct grid *gd; struct grid_line *gl; u_int cursor_x, cursor_y, prompt_x = 0; u_int prompt_y = UINT_MAX, y, total; int found_start = 0, found_end = 0; int next_prompt, in_range, found; - void *buf; - size_t len; - cursor_x = data->cx; - cursor_y = screen_hsize(data->backing) + data->cy - data->oy; + if (data->source == NULL) + return (0); + window_copy_cursor_source(wme, &cursor_x, &cursor_y); log_debug("%s: cursor at %u,%u", __func__, cursor_x, cursor_y); + gd = data->source->grid; total = gd->hsize + gd->sy; for (y = 0; y < total; y++) { gl = grid_get_line(gd, y); @@ -6292,8 +7348,8 @@ window_copy_find_output_range(struct window_mode_entry *wme, u_int *sx, } if (!found_start) { log_debug("%s: no output after prompt", __func__); - found = window_copy_find_previous_output_range(wme, cursor_x, - cursor_y, sx, sy, ex, ey); + found = window_copy_find_previous_output_range(data->source, + cursor_x, cursor_y, sx, sy, ex, ey); return (found); } if (!found_end) { @@ -6301,16 +7357,15 @@ window_copy_find_output_range(struct window_mode_entry *wme, u_int *sx, log_debug("%s: output interrupted by next prompt", __func__); return (0); } - window_copy_output_end(data->backing, ex, ey); + window_copy_output_end(data->source, ex, ey); } - buf = window_copy_get_grid_range(wme, *sx, *sy, *ex, *ey, &len); - if (buf == NULL) { - log_debug("%s: empty output", __func__); - found = window_copy_find_previous_output_range(wme, cursor_x, - cursor_y, sx, sy, ex, ey); + if (*sx == *ex && *sy == *ey) { + log_debug("%s: output at cursor is empty, look for previous", + __func__); + found = window_copy_find_previous_output_range(data->source, + cursor_x, cursor_y, sx, sy, ex, ey); return (found); } - free(buf); log_debug("%s: output from %u,%u to %u,%u", __func__, *sx, *sy, *ex, *ey); return (1); @@ -6318,26 +7373,23 @@ window_copy_find_output_range(struct window_mode_entry *wme, u_int *sx, /* Find the most recent complete output at or before a position. */ static int -window_copy_find_previous_output_range(struct window_mode_entry *wme, - u_int cursor_x, u_int cursor_y, u_int *sx, u_int *sy, u_int *ex, - u_int *ey) +window_copy_find_previous_output_range(struct screen *s, u_int cursor_x, + u_int cursor_y, u_int *sx, u_int *sy, u_int *ex, u_int *ey) { - struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; + struct grid *gd = s->grid; struct grid_line *gl; struct osc133_data *od; - void *buf; u_int start_x, start_y, end_x, end_y; u_int y, total; - size_t len; - int found = 0, have_prompt = 0; - int pending = 0; + int found = 0, found_empty = 0, have_prompt = 0; + int pending = 0, started_here; int has_start, has_end, end_first; total = gd->hsize + gd->sy; for (y = 0; y < total && y <= cursor_y; y++) { gl = grid_get_line(gd, y); od = &gl->osc133_data; + started_here = 0; has_start = (gl->flags & GRID_LINE_START_OUTPUT && (y != cursor_y || od->out_start_col <= cursor_x)); has_end = (gl->flags & GRID_LINE_END_OUTPUT && @@ -6349,6 +7401,7 @@ window_copy_find_previous_output_range(struct window_mode_entry *wme, start_x = od->out_start_col; start_y = y; pending = 1; + started_here = 1; } /* The output may have cleared its C marker from the screen. */ if (!pending && !have_prompt && @@ -6357,19 +7410,27 @@ window_copy_find_previous_output_range(struct window_mode_entry *wme, od->out_end_col <= od->prompt_col)) { start_x = start_y = 0; pending = 1; + started_here = 1; } if (pending && has_end) { end_x = od->out_end_col; end_y = y; - buf = window_copy_get_grid_range(wme, start_x, start_y, - end_x, end_y, &len); - if (buf != NULL) { - free(buf); + + /* + * Prefer a previously found non-empty output over a + * later one that turns out to be empty (for example a + * command with no output immediately followed by the + * cursor's prompt). + */ + if (!found || found_empty || + start_x != end_x || start_y != end_y) { *sx = start_x; *sy = start_y; *ex = end_x; *ey = end_y; found = 1; + found_empty = (start_x == end_x && + start_y == end_y); } pending = 0; } @@ -6377,12 +7438,14 @@ window_copy_find_previous_output_range(struct window_mode_entry *wme, start_x = od->out_start_col; start_y = y; pending = 1; + started_here = 1; } if (gl->flags & GRID_LINE_START_PROMPT) { - /* Output may start after the prompt. */ - if (!has_start || od->out_start_col < od->prompt_col) - pending = 0; have_prompt = 1; + /* Output may start after the prompt. */ + if (!started_here || + (has_start && od->out_start_col < od->prompt_col)) + pending = 0; } } return (found); @@ -6399,9 +7462,14 @@ static void * window_copy_get_output(struct window_mode_entry *wme, size_t *len, int all) { struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; + struct grid *gd; u_int sx, sy, ex, ey, total; + if (data->source == NULL) { + *len = 0; + return (NULL); + } + gd = data->source->grid; total = gd->hsize + gd->sy; if (all) { sx = sy = 0; @@ -6411,15 +7479,13 @@ window_copy_get_output(struct window_mode_entry *wme, size_t *len, int all) *len = 0; return (NULL); } - return (window_copy_get_grid_range(wme, sx, sy, ex, ey, len)); + return (window_copy_get_grid_range(gd, sx, sy, ex, ey, len)); } static void * -window_copy_get_grid_range(struct window_mode_entry *wme, u_int sx, u_int sy, +window_copy_get_grid_range(struct grid *gd, u_int sx, u_int sy, u_int ex, u_int ey, size_t *len) { - struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; char *buf; size_t off; u_int i; @@ -6427,7 +7493,7 @@ window_copy_get_grid_range(struct window_mode_entry *wme, u_int sx, u_int sy, buf = xmalloc(1); off = 0; for (i = sy; i <= ey; i++) { - window_copy_copy_line(wme, &buf, &off, i, i == sy ? sx : 0, + window_copy_copy_grid_line(gd, &buf, &off, i, i == sy ? sx : 0, i == ey ? ex : gd->sx); } if (off != 0 && buf[off - 1] == '\n') @@ -6698,7 +7764,14 @@ window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, u_int sy, u_int sx, u_int ex) { struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; + + window_copy_copy_grid_line(data->backing->grid, buf, off, sy, sx, ex); +} + +static void +window_copy_copy_grid_line(struct grid *gd, char **buf, size_t *off, u_int sy, + u_int sx, u_int ex) +{ struct grid_cell gc; struct grid_line *gl; struct utf8_data ud; @@ -6720,7 +7793,7 @@ window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, if (wrapped) xx = gl->cellsize; else - xx = window_copy_find_length(wme, sy); + xx = grid_line_length(gd, sy); if (ex > xx) ex = xx; if (sx > xx) @@ -7409,8 +8482,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) return; } if (window_copy_line_numbers_active(wme)) { - if (window_copy_line_number_mode(wme) != - WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { + if (!window_copy_line_number_is_absolute(wme)) { window_copy_redraw_screen(wme); return; } @@ -7480,8 +8552,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) return; } if (window_copy_line_numbers_active(wme)) { - if (window_copy_line_number_mode(wme) != - WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { + if (!window_copy_line_number_is_absolute(wme)) { window_copy_redraw_screen(wme); return; }