diff --git a/usr.bin/tmux/grid.c b/usr.bin/tmux/grid.c index 813c43f2088..89b5d1d8a5d 100644 --- a/usr.bin/tmux/grid.c +++ b/usr.bin/tmux/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.148 2026/06/07 20:03:02 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.149 2026/06/10 14:29:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -292,7 +292,7 @@ grid_free_line(struct grid *gd, u_int py) } /* Free several lines. */ -static void +void grid_free_lines(struct grid *gd, u_int py, u_int ny) { u_int yy; @@ -320,6 +320,10 @@ grid_create(u_int sx, u_int sy, u_int hlimit) gd->hsize = 0; gd->hlimit = hlimit; + gd->scroll_added = 0; + gd->scroll_collected = 0; + gd->scroll_generation = 0; + if (gd->sy != 0) gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata); else @@ -405,6 +409,7 @@ grid_collect_history(struct grid *gd, int all) grid_trim_history(gd, ny); gd->hsize -= ny; + gd->scroll_collected += ny; if (gd->hscrolled > gd->hsize) gd->hscrolled = gd->hsize; } @@ -442,6 +447,7 @@ grid_scroll_history(struct grid *gd, u_int bg) grid_compact_line(&gd->linedata[gd->hsize]); gd->linedata[gd->hsize].time = current_time; gd->hsize++; + gd->scroll_added++; } /* Clear the history. */ @@ -452,6 +458,7 @@ grid_clear_history(struct grid *gd) gd->hscrolled = 0; gd->hsize = 0; + gd->scroll_generation++; gd->linedata = xreallocarray(gd->linedata, gd->sy, sizeof *gd->linedata); @@ -489,6 +496,7 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) /* Move the history offset down over the line. */ gd->hscrolled++; gd->hsize++; + gd->scroll_added++; } /* Expand line to fit to cell. */ @@ -1510,6 +1518,7 @@ grid_reflow(struct grid *gd, u_int sx) free(gd->linedata); gd->linedata = target->linedata; free(target); + gd->scroll_generation++; } /* Convert to position based on wrapped lines. */ diff --git a/usr.bin/tmux/key-bindings.c b/usr.bin/tmux/key-bindings.c index 4b8d41e7c21..ebf34a90130 100644 --- a/usr.bin/tmux/key-bindings.c +++ b/usr.bin/tmux/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.171 2026/06/07 08:25:59 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.172 2026/06/10 14:29:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -530,7 +530,7 @@ key_bindings_init(void) "bind -Tcopy-mode g { command-prompt -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode q { send -X cancel }", - "bind -Tcopy-mode r { send -X refresh-from-pane }", + "bind -Tcopy-mode r { send -X refresh-toggle }", "bind -Tcopy-mode t { command-prompt -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 }", @@ -638,7 +638,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi n { send -X search-again }", "bind -Tcopy-mode-vi o { send -X other-end }", "bind -Tcopy-mode-vi q { send -X cancel }", - "bind -Tcopy-mode-vi r { send -X refresh-from-pane }", + "bind -Tcopy-mode-vi r { send -X refresh-toggle }", "bind -Tcopy-mode-vi t { command-prompt -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode-vi v { send -X rectangle-toggle }", "bind -Tcopy-mode-vi w { send -X next-word }", diff --git a/usr.bin/tmux/tmux.1 b/usr.bin/tmux/tmux.1 index 66c7f5c9381..b35dfab5b7d 100644 --- a/usr.bin/tmux/tmux.1 +++ b/usr.bin/tmux/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1077 2026/06/09 12:51:57 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1078 2026/06/10 14:29:08 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: June 9 2026 $ +.Dd $Mdocdate: June 10 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2293,11 +2293,22 @@ Toggle rectangle selection mode. .Xc Cycles the current line between centre, top, and bottom. .It Xo -.Ic refresh\-from\-pane +.Ic refresh\-on +.Xc +Turn on automatic refresh of the content from the pane, so that new output +appears while in copy mode. +Automatic refresh is off by default; it will scroll only while the cursor is at +the bottom and is paused while a selection is in progress. +.It Xo +.Ic refresh\-off +.Xc +Turn off automatic refresh of the content from the pane. +.It Xo +.Ic refresh\-toggle (vi: r) (emacs: r) .Xc -Refresh the content from the pane. +Toggle automatic refresh of the content from the pane. .It Xo .Ic scroll\-bottom .Xc diff --git a/usr.bin/tmux/tmux.h b/usr.bin/tmux/tmux.h index 09fe8080aba..f9c09ad79f1 100644 --- a/usr.bin/tmux/tmux.h +++ b/usr.bin/tmux/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1341 2026/06/09 21:22:22 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1342 2026/06/10 14:29:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -862,6 +862,10 @@ struct grid { u_int hsize; u_int hlimit; + u_int scroll_added; + u_int scroll_collected; + u_int scroll_generation; + struct grid_line *linedata; }; @@ -3168,6 +3172,7 @@ int grid_cells_look_equal(const struct grid_cell *, const struct grid_cell *); struct grid *grid_create(u_int, u_int, u_int); void grid_destroy(struct grid *); +void grid_free_lines(struct grid *, u_int, u_int); int grid_compare(struct grid *, struct grid *); void grid_collect_history(struct grid *, int); void grid_remove_history(struct grid *, u_int ); diff --git a/usr.bin/tmux/window-copy.c b/usr.bin/tmux/window-copy.c index b38fcb26eb0..818e07bc684 100644 --- a/usr.bin/tmux/window-copy.c +++ b/usr.bin/tmux/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.403 2026/06/09 21:22:22 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.404 2026/06/10 14:29:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -51,6 +51,11 @@ static void window_copy_redraw_selection(struct window_mode_entry *, u_int); static void window_copy_redraw_lines(struct window_mode_entry *, u_int, u_int); static void window_copy_redraw_screen(struct window_mode_entry *); +static void window_copy_do_refresh(struct window_mode_entry *, int); +static void window_copy_refresh_timer(int, short, void *); +static void window_copy_refresh_arm(struct window_mode_entry *); +static void window_copy_refresh_start(struct window_mode_entry *); +static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *); static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); @@ -255,6 +260,10 @@ struct window_copy_mode_data { int backing_written; /* backing display started */ struct input_ctx *ictx; + u_int sync_added; /* snapshot of backing grid counters */ + u_int sync_collected; + u_int sync_generation; + int viewmode; /* view mode entered */ u_int oy; /* number of lines scrolled up */ @@ -338,6 +347,10 @@ struct window_copy_mode_data { struct event dragtimer; #define WINDOW_COPY_DRAG_REPEAT_TIME 50000 + + struct event refresh_timer; +#define WINDOW_COPY_REFRESH_INTERVAL 50000 + int refresh_active; }; static void @@ -424,6 +437,116 @@ window_copy_clone_screen(struct screen *src, struct screen *hint, u_int *cx, return (dst); } +/* + * 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. + */ +static void +window_copy_sync_snapshot(struct window_copy_mode_data *data, struct grid *src) +{ + data->sync_added = src->scroll_added; + data->sync_collected = src->scroll_collected; + data->sync_generation = src->scroll_generation; +} + +/* + * Reconcile the backing screen with the live pane grid in place, copying only + * the history that scrolled in or was collected since the last snapshot rather + * than cloning the whole scrollback. The result is identical to a fresh + * window_copy_clone_screen, so the caller repositions and redraws the same way + * for both paths. Returns 1 on success, or 0 if the caller must fall back to a + * full clone (different source pane, geometry or generation change, or counter + * deltas that do not add up). + */ +static int +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 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; + + /* + * 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) + return (0); + + /* Indices only line up at the same size and generation. */ + if (sg->sx != dg->sx || sg->sy != dg->sy || + sg->scroll_generation != data->sync_generation) + return (0); + + added = sg->scroll_added - data->sync_added; + collected = sg->scroll_collected - data->sync_collected; + + /* + * Reject anything that does not balance: counter wrap, a history-limit + * change that collected past the snapshot, or arithmetic that does not + * reproduce the new history size. + */ + if (added > (u_int)INT_MAX || collected > (u_int)INT_MAX || + collected > old_hsize || old_hsize + added < collected || + old_hsize + added - collected != new_hsize) + return (0); + + kept = old_hsize - collected; + + if (added == 0 && collected == 0) { + /* History is unchanged; only the viewport can have mutated. */ + grid_duplicate_lines(dg, dg->hsize, sg, sg->hsize, sy); + } else { + /* Drop the oldest lines and shift the rest down. */ + if (collected > 0) { + grid_free_lines(dg, 0, collected); + memmove(&dg->linedata[0], &dg->linedata[collected], + (old_hsize + sy - collected) * sizeof *dg->linedata); + memset(&dg->linedata[old_hsize + sy - collected], 0, + collected * sizeof *dg->linedata); + } + + /* Resize linedata to the new history plus viewport. */ + if (new_hsize + sy != old_hsize + sy - collected) { + dg->linedata = xreallocarray(dg->linedata, + new_hsize + sy, sizeof *dg->linedata); + memset(&dg->linedata[old_hsize + sy - collected], 0, + (new_hsize - kept) * sizeof *dg->linedata); + } + + /* + * Set hsize before copying so grid_duplicate_lines does not + * clamp the count to the old, smaller grid size. + */ + dg->hsize = new_hsize; + + /* Copy the newly scrolled history, then refresh the viewport. */ + if (added > 0) + grid_duplicate_lines(dg, kept, sg, kept, added); + grid_duplicate_lines(dg, new_hsize, sg, new_hsize, sy); + } + + dg->hscrolled = sg->hscrolled; + + /* Match clone_screen's backing cursor placement. */ + if (src->cy > dg->sy - 1) { + dst->cx = 0; + dst->cy = dg->sy - 1; + } else { + dst->cx = src->cx; + dst->cy = src->cy; + } + + return (1); +} + static struct window_copy_mode_data * window_copy_common_init(struct window_mode_entry *wme) { @@ -458,6 +581,7 @@ window_copy_common_init(struct window_mode_entry *wme) data->modekeys = options_get_number(wp->window->options, "mode-keys"); evtimer_set(&data->dragtimer, window_copy_scroll_timer, wme); + evtimer_set(&data->refresh_timer, window_copy_refresh_timer, wme); return (data); } @@ -475,6 +599,7 @@ window_copy_init(struct window_mode_entry *wme, data = window_copy_common_init(wme); data->backing = window_copy_clone_screen(base, &data->screen, &cx, &cy, wme->swp != wme->wp); + window_copy_sync_snapshot(data, base->grid); data->cx = cx; if (cy < screen_hsize(data->backing)) { @@ -541,6 +666,7 @@ window_copy_free(struct window_mode_entry *wme) struct window_copy_mode_data *data = wme->data; evtimer_del(&data->dragtimer); + evtimer_del(&data->refresh_timer); free(data->searchmark); free(data->searchstr); @@ -2781,34 +2907,136 @@ window_copy_cmd_search_forward_incremental(struct window_copy_cmd_state *cs) return (action); } -static enum window_copy_cmd_action -window_copy_cmd_refresh_from_pane(struct window_copy_cmd_state *cs) +/* + * Reconcile the backing screen with the live pane, incrementally if possible + * and otherwise by recloning, then reposition the view. When following, jump + * to the bottom so new output stays visible; otherwise keep the same lines on + * screen. Driven by the automatic refresh timer. + */ +static void +window_copy_do_refresh(struct window_mode_entry *wme, int follow) { - struct window_mode_entry *wme = cs->wme; struct window_pane *wp = wme->swp; struct window_copy_mode_data *data = wme->data; u_int oy_from_top; - if (data->viewmode) - return (WINDOW_COPY_CMD_NOTHING); if (data->oy > screen_hsize(data->backing)) data->oy = screen_hsize(data->backing); oy_from_top = screen_hsize(data->backing) - data->oy; - screen_free(data->backing); - free(data->backing); - data->backing = window_copy_clone_screen(&wp->base, &data->screen, NULL, - NULL, wme->swp != wme->wp); + if (!window_copy_sync_backing(wme)) { + screen_free(data->backing); + free(data->backing); + data->backing = window_copy_clone_screen(&wp->base, + &data->screen, NULL, NULL, wme->swp != wme->wp); + } - if (oy_from_top <= screen_hsize(data->backing)) + if (follow) { + data->cy = screen_size_y(&data->screen) - 1; + data->cx = window_copy_cursor_limit(wme, + screen_hsize(data->backing) + data->cy, 0); + data->oy = 0; + } else if (oy_from_top <= screen_hsize(data->backing)) data->oy = screen_hsize(data->backing) - oy_from_top; else { data->cy = 0; data->oy = screen_hsize(data->backing); } + window_copy_sync_snapshot(data, wp->base.grid); window_copy_size_changed(wme); - return (WINDOW_COPY_CMD_REDRAW); +} + +static void +window_copy_refresh_arm(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + struct timeval tv = { + .tv_sec = WINDOW_COPY_REFRESH_INTERVAL / 1000000, + .tv_usec = WINDOW_COPY_REFRESH_INTERVAL % 1000000 + }; + + if (data->refresh_active) + evtimer_add(&data->refresh_timer, &tv); +} + +static void +window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) +{ + struct window_mode_entry *wme = arg; + struct window_pane *wp = wme->wp; + struct window_copy_mode_data *data = wme->data; + int follow; + + if (TAILQ_FIRST(&wp->modes) != wme || !data->refresh_active) + return; + + /* + * Skip the refresh while a selection is being made, otherwise it would + * move; only follow new output if the cursor is still at the bottom. + */ + if ((wp->flags & PANE_UNSEENCHANGES) && data->screen.sel == NULL && + data->cursordrag == CURSORDRAG_NONE) { + follow = (data->oy == 0 && + data->cy == screen_size_y(&data->screen) - 1); + window_copy_do_refresh(wme, follow); + window_copy_redraw_screen(wme); + /* The timer runs outside key handling, so force a repaint. */ + wp->flags |= PANE_REDRAW; + wp->flags &= ~PANE_UNSEENCHANGES; + } + + window_copy_refresh_arm(wme); +} + +static void +window_copy_refresh_start(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + /* + * Do not refresh a view of another pane (copy-mode -s): the source may + * disappear and changes are not tracked on this pane. + */ + if (data->viewmode || wme->swp != wme->wp || data->refresh_active) + return; + data->refresh_active = 1; + window_copy_refresh_arm(wme); +} + +static void +window_copy_refresh_stop(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + data->refresh_active = 0; + evtimer_del(&data->refresh_timer); +} + +static enum window_copy_cmd_action +window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) +{ + window_copy_refresh_start(cs->wme); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_refresh_off(struct window_copy_cmd_state *cs) +{ + window_copy_refresh_stop(cs->wme); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_refresh_toggle(struct window_copy_cmd_state *cs) +{ + struct window_copy_mode_data *data = cs->wme->data; + + if (data->refresh_active) + window_copy_refresh_stop(cs->wme); + else + window_copy_refresh_start(cs->wme); + return (WINDOW_COPY_CMD_NOTHING); } static enum window_copy_cmd_action @@ -3275,11 +3503,23 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_rectangle_toggle }, - { .command = "refresh-from-pane", + { .command = "refresh-on", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, - .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, - .f = window_copy_cmd_refresh_from_pane + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_on + }, + { .command = "refresh-off", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_off + }, + { .command = "refresh-toggle", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_toggle }, { .command = "scroll-bottom", .args = { "", 0, 0, NULL },