diff --git a/grid-reader.c b/grid-reader.c index 4cf47b8cb..b192e7a7b 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */ +/* $OpenBSD: grid-reader.c,v 1.12 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee @@ -436,3 +436,208 @@ grid_reader_cursor_back_to_indentation(struct grid_reader *gr) gr->cx = oldx; gr->cy = oldy; } + +/* Get the end of the last used line. */ +static void +grid_reader_output_end(struct grid *gd, u_int *x, u_int *y) +{ + u_int last = gd->hsize + gd->sy - 1; + + while (last > 0 && grid_get_line(gd, last)->cellused == 0) + last--; + *x = grid_get_line(gd, last)->cellused; + *y = last; +} + +/* Find the most recent complete output at or before the cursor. */ +static int +grid_reader_previous_output_range(struct grid_reader *gr, u_int *sx, + u_int *sy, u_int *ex, u_int *ey) +{ + struct grid *gd = gr->gd; + struct grid_line *gl; + struct osc133_data *od; + u_int cursor_x = gr->cx, cursor_y = gr->cy; + u_int start_x, start_y, y, total; + int found = 0, have_prompt = 0; + int pending = 0; + int has_start, has_end, end_first; + int cleared; + + total = gd->hsize + gd->sy; + + /* + * Scan from the top because markers arrive in their natural order, C + * then D. A backward scan would meet D first and need extra state to + * find its C. The last complete output found is the one wanted. + */ + for (y = 0; y < total && y <= cursor_y; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + + /* On the cursor's line, ignore markers after the cursor. */ + has_start = has_end = end_first = 0; + if (gl->flags & GRID_LINE_START_OUTPUT) { + if (y != cursor_y || od->out_start_col <= cursor_x) + has_start = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (y != cursor_y || od->out_end_col <= cursor_x) + has_end = 1; + } + + /* An end before the start ends the previous output. */ + if (has_start && has_end && od->out_end_col < od->out_start_col) + end_first = 1; + + /* A C marker starts an output that is pending until its D. */ + if (has_start && !end_first) { + start_x = od->out_start_col; + start_y = y; + pending = 1; + } + + /* The output may have cleared its C marker from the screen. */ + if (!pending && !have_prompt && + (gl->flags & GRID_LINE_END_OUTPUT)) { + cleared = 1; + if ((gl->flags & GRID_LINE_START_PROMPT) && + od->out_end_col > od->prompt_col) + cleared = 0; + if (cleared) { + start_x = start_y = 0; + pending = 1; + } + } + + /* A D marker completes the pending output. */ + if (pending && has_end) { + *sx = start_x; + *sy = start_y; + *ex = od->out_end_col; + *ey = y; + found = 1; + pending = 0; + } + if (has_start && end_first) { + start_x = od->out_start_col; + start_y = y; + pending = 1; + } + + /* A prompt drops an unfinished output. */ + if (gl->flags & GRID_LINE_START_PROMPT) { + if (!has_start || od->out_start_col < od->prompt_col) + pending = 0; + have_prompt = 1; + } + } + return (found); +} + +/* + * Find the output range for the command at the cursor, from its C marker to + * its D marker. If the prompt has no output yet, use the previous output. + * Returns 0 if there is no usable range. + */ +int +grid_reader_output_range(struct grid_reader *gr, u_int *sx, u_int *sy, + u_int *ex, u_int *ey) +{ + struct grid *gd = gr->gd; + struct grid_line *gl; + struct osc133_data *od; + u_int cursor_x = gr->cx, cursor_y = gr->cy; + u_int prompt_x = 0, prompt_y = UINT_MAX; + u_int y, total; + int found_start = 0, found_end = 0; + int next_prompt, in_range, end_ok; + int found; + + /* Find the last prompt at or before the cursor. */ + total = gd->hsize + gd->sy; + for (y = 0; y < total; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + if (~gl->flags & GRID_LINE_START_PROMPT) + continue; + if (y > cursor_y) + break; + if (y == cursor_y && od->prompt_col > cursor_x) + break; + prompt_y = y; + prompt_x = od->prompt_col; + } + + /* With no prompt, its A marker may have left history, so start at 0. */ + if (prompt_y == UINT_MAX) + y = 0; + else + y = prompt_y; + + /* Walk down from the prompt to its C and D, up to the next prompt. */ + for (; y < total; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + next_prompt = 0; + if (y != prompt_y && (gl->flags & GRID_LINE_START_PROMPT)) + next_prompt = 1; + + /* Output before the next prompt on its line is ours. */ + if (y == prompt_y) + in_range = (od->out_start_col >= prompt_x); + else if (next_prompt) + in_range = (od->out_start_col < od->prompt_col); + else + in_range = 1; + if (gl->flags & GRID_LINE_START_OUTPUT && in_range) { + *sx = od->out_start_col; + *sy = y; + found_start = 1; + } + + /* Both A and C may have left history while D remains. */ + if (!found_start && prompt_y == UINT_MAX && + (gl->flags & GRID_LINE_END_OUTPUT)) { + if (!next_prompt || od->out_end_col <= od->prompt_col) { + *sx = *sy = 0; + found_start = 1; + } + } + + /* An output may end on the same line or the next prompt's. */ + if (found_start && (gl->flags & GRID_LINE_END_OUTPUT)) { + end_ok = 1; + if (y == prompt_y && od->out_end_col < prompt_x) + end_ok = 0; + if (y == *sy && od->out_end_col < *sx) + end_ok = 0; + if (end_ok) { + *ex = od->out_end_col; + *ey = y; + found_end = 1; + break; + } + } + if (next_prompt) + break; + } + + /* The cursor is on a prompt with no output yet. */ + if (!found_start) { + found = grid_reader_previous_output_range(gr, sx, sy, ex, ey); + return (found); + } + + /* + * Without a D marker the command is still running, so its output runs + * to the last used line. If the next prompt came first, it has no + * usable end. + */ + if (!found_end) { + if (y != total) + return (0); + grid_reader_output_end(gd, ex, ey); + } + return (1); +} diff --git a/grid.c b/grid.c index 7539d72ef..19d1054ef 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.159 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1296,12 +1296,60 @@ grid_reflow_move(struct grid *gd, struct grid_line *from) return (to); } +/* Move the OSC 133 markers from one line to another. */ +static void +grid_reflow_move_osc133(struct grid_line *to, struct grid_line *from, + u_int to_col, u_int count, int last) +{ + struct osc133_data *src = &from->osc133_data; + struct osc133_data *dst = &to->osc133_data; + u_short *src_col[] = { + &src->prompt_col, + &src->cmd_col, + &src->out_start_col, + &src->out_end_col + }; + u_short *dst_col[] = { + &dst->prompt_col, + &dst->cmd_col, + &dst->out_start_col, + &dst->out_end_col + }; + u_int flags[] = { + GRID_LINE_START_PROMPT|GRID_LINE_SECOND_PROMPT, + GRID_LINE_START_COMMAND, + GRID_LINE_START_OUTPUT, + GRID_LINE_END_OUTPUT + }; + u_int i, flag; + + if ((from->flags & GRID_LINE_OSC133_FLAGS) == 0) + return; + for (i = 0; i < nitems(flags); i++) { + flag = from->flags & flags[i]; + if (flag == 0) + continue; + if (!last && *src_col[i] >= count) { + *src_col[i] -= count; + continue; + } + *dst_col[i] = to_col + *src_col[i]; + to->flags |= flag; + from->flags &= ~flag; + *src_col[i] = 0; + if (flag == GRID_LINE_END_OUTPUT) { + dst->exit_status = src->exit_status; + src->exit_status = 0; + } + } +} + /* Join line below onto this one. */ static void grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, u_int width, int already) { - struct grid_line *gl, *from = NULL; + struct grid_line *gl, *from = NULL, *next; struct grid_cell gc; u_int lines, left, i, to, line, want = 0; u_int at; @@ -1332,13 +1380,20 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, break; line = yy + 1 + lines; - /* If the next line is empty, skip it. */ + /* Consume empty lines, including any markers. */ if (~gd->linedata[line].flags & GRID_LINE_WRAPPED) wrapped = 0; if (gd->linedata[line].cellused == 0) { + next = &gd->linedata[line]; + if (!wrapped && + (next->flags & GRID_LINE_OSC133_FLAGS) == 0) + break; + from = next; + want = 0; + grid_reflow_move_osc133(gl, from, at, 0, 1); + lines++; if (!wrapped) break; - lines++; continue; } @@ -1365,6 +1420,8 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, grid_set_cell(target, at, to, &gc); at++; } + grid_reflow_move_osc133(gl, from, at - want, want, + want == from->cellused); lines++; /* @@ -1435,6 +1492,14 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, line = target->sy + 1; first = grid_reflow_add(target, lines); + /* The first line keeps only the markers within its first at cells. */ + memcpy(first, gl, sizeof *first); + first->cellsize = first->cellused = at; + first->flags &= ~GRID_LINE_OSC133_FLAGS; + first->flags |= GRID_LINE_WRAPPED; + memset(&first->osc133_data, 0, sizeof first->osc133_data); + grid_reflow_move_osc133(first, gl, 0, at, 0); + /* Copy sections from the original line. */ width = 0; xx = 0; @@ -1442,6 +1507,8 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, grid_get_cell1(gl, i, &gc); if (width + gc.data.width > sx) { target->linedata[line].flags |= GRID_LINE_WRAPPED; + grid_reflow_move_osc133(&target->linedata[line], gl, 0, + xx, 0); line++; width = 0; @@ -1453,11 +1520,7 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, } if (flags & GRID_LINE_WRAPPED) target->linedata[line].flags |= GRID_LINE_WRAPPED; - - /* Move the remainder of the original line. */ - gl->cellsize = gl->cellused = at; - gl->flags |= GRID_LINE_WRAPPED; - memcpy(first, gl, sizeof *first); + grid_reflow_move_osc133(&target->linedata[line], gl, 0, xx, 1); grid_reflow_dead(gl); /* Adjust the scroll position. */ diff --git a/input.c b/input.c index d5515df33..24d55174b 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.272 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.273 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3241,7 +3241,6 @@ input_osc_133(struct input_ctx *ictx, const char *p) case 'A': case 'N': if (gl != NULL) { - memset(&gl->osc133_data, 0, sizeof gl->osc133_data); gl->osc133_data.prompt_col = s->cx; gl->flags |= GRID_LINE_START_PROMPT; } diff --git a/key-bindings.c b/key-bindings.c index 786017407..d8ba0cceb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.193 2026/09/21 20:45:48 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.194 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -53,6 +53,7 @@ " '#{?#{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},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}" \ " ''" \ " '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \ @@ -575,6 +576,7 @@ key_bindings_init(void) "bind -Tcopy-mode C-l { send -X recentre-top-bottom }", "bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }", "bind -Tcopy-mode C-n { send -X cursor-down }", + "bind -Tcopy-mode C-o { send -X select-output }", "bind -Tcopy-mode C-p { send -X cursor-up }", "bind -Tcopy-mode C-r { command-prompt -P -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }", "bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }", @@ -592,6 +594,7 @@ key_bindings_init(void) "bind -Tcopy-mode R { send -X rectangle-toggle }", "bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", "bind -Tcopy-mode X { send -X set-mark }", + "bind -Tcopy-mode e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }", "bind -Tcopy-mode f { command-prompt -P -1p'(jump forward)' { send -X jump-forward -- '%%' } }", "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", @@ -687,6 +690,8 @@ key_bindings_init(void) "bind -Tcopy-mode-vi K { send -X scroll-up }", "bind -Tcopy-mode-vi L { send -X bottom-line }", "bind -Tcopy-mode-vi M { send -X middle-line }", + "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 P { send -X toggle-position }", "bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", diff --git a/spawn.c b/spawn.c index 2a0c73a41..19bd14a7d 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.56 2026/10/08 07:06:26 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.57 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -685,7 +685,7 @@ spawn_editor_finish(struct window_pane *wp) struct spawn_editor_state * spawn_editor(struct client *c, const char *buf, size_t len, - spawn_finish_edit_cb cb, void *arg) + const char *editor, spawn_finish_edit_cb cb, void *arg) { struct spawn_editor_state *es; struct spawn_context sc = { 0 }; @@ -699,13 +699,13 @@ spawn_editor(struct client *c, const char *buf, size_t len, FILE *f; char *cmd, *cause = NULL; char path[] = _PATH_TMP "tmux.XXXXXXXX"; - const char *editor; int fd; if (w->modal != NULL) return (NULL); - editor = options_get_string(global_options, "editor"); + if (editor == NULL) + editor = options_get_string(global_options, "editor"); fd = mkstemp(path); if (fd == -1) return (NULL); diff --git a/tmux.1 b/tmux.1 index 8893e075c..d2f0b751f 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1180 2026/10/07 13:13:49 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1181 2026/10/08 07:50:05 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: October 7 2026 $ +.Dd $Mdocdate: October 8 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2011,6 +2011,25 @@ Same as .Ic copy\-pipe but also exit copy mode. .It Xo +.Ic copy\-pipe\-output +.Op Fl aCP +.Op Ar command +.Op Ar prefix +.Xc +Copy the current OSC 133 command output, pipe it to +.Ar command , +and clear the selection. +.Ar prefix +is used to name the new paste buffer. +.It Xo +.Ic copy\-output +.Op Fl aCP +.Op Ar prefix +.Xc +Copy the current OSC 133 command output and clear the selection. +.Ar prefix +is used to name the new paste buffer. +.It Xo .Ic copy\-selection .Op Fl CP .Op Ar prefix @@ -2229,6 +2248,31 @@ but treat all non-whitespace characters as part of a word. .Xc Switch at which end of the selection the cursor sits. .It Xo +.Ic open\-output +.Op Fl a +.Op Ar command +(vi: M\-e) +(emacs: e) +.Xc +Open the current OSC 133 command output in an editor. +If +.Ar command +is given, it is used as the editor command; otherwise, the +.Ic editor +option is used. +.It Xo +.Ic open\-selection +.Op Ar command +(vi: M\-e) +(emacs: e) +.Xc +Open the current selection in an editor. +If +.Ar command +is given, it is used as the editor command; otherwise, the +.Ic editor +option is used. +.It Xo .Ic page\-down (vi: C\-f) (emacs: PageDown) @@ -2269,6 +2313,21 @@ Same as .Ic pipe but also exit copy mode. .It Xo +.Ic pipe\-output +.Op Fl a +.Op Ar command +.Xc +Pipe the current OSC 133 command output to +.Ar command +and clear the selection. +.It Xo +.Ic pipe\-selection +.Op Ar command +.Xc +Pipe the selected text to +.Ar command +and clear the selection. +.It Xo .Ic previous\-matching\-bracket (emacs: M\-C\-b) .Xc @@ -2458,6 +2517,13 @@ Search forward for the specified plain text. Repeat the last search in the reverse direction (forward becomes backward and backward becomes forward). .It Xo +.Ic select\-output +.Op Fl a +(vi: M\-o) +(emacs: C\-o) +.Xc +Select the current OSC 133 command output. +.It Xo .Ic select\-line (vi: V) .Xc diff --git a/tmux.h b/tmux.h index a23019f4c..c442066b5 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1453 2026/10/06 17:49:45 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1454 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3487,6 +3487,8 @@ void grid_reader_start(struct grid_reader *, struct grid *, u_int, u_int); void grid_reader_get_cursor(struct grid_reader *, u_int *, u_int *); u_int grid_reader_line_length(struct grid_reader *); int grid_reader_in_set(struct grid_reader *, const char *); +int grid_reader_output_range(struct grid_reader *, u_int *, u_int *, + u_int *, u_int *); void grid_reader_cursor_right(struct grid_reader *, int, int, int); void grid_reader_cursor_left(struct grid_reader *, int); void grid_reader_cursor_down(struct grid_reader *); @@ -4171,7 +4173,7 @@ struct winlink *spawn_window(struct spawn_context *, char **); struct window_pane *spawn_pane(struct spawn_context *, char **); typedef void (*spawn_finish_edit_cb)(char *, size_t, void *); struct spawn_editor_state *spawn_editor(struct client *, const char *, size_t, - spawn_finish_edit_cb, void *); + const char *, spawn_finish_edit_cb, void *); void spawn_cancel_editor(struct spawn_editor_state *); pid_t spawn_get_editor_pid(struct spawn_editor_state *); void spawn_editor_finish(struct window_pane *); diff --git a/window-buffer.c b/window-buffer.c index f68e76f85..e3168ec27 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.53 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -606,7 +606,8 @@ window_buffer_start_edit(struct window_buffer_modedata *data, ed->name = xstrdup(paste_buffer_name(pb)); ed->pb = pb; - ed->editor = spawn_editor(c, buf, len, window_buffer_edit_close_cb, ed); + ed->editor = spawn_editor(c, buf, len, NULL, + window_buffer_edit_close_cb, ed); if (ed->editor == NULL) window_buffer_finish_edit(ed); else { diff --git a/window-copy.c b/window-copy.c index 48413ce87..3d40ffe24 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.436 2026/10/07 12:59:50 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.437 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -121,9 +121,17 @@ static int window_copy_set_selection(struct window_mode_entry *, int, int); static int window_copy_update_selection(struct window_mode_entry *, int, int); static void window_copy_synchronize_cursor(struct window_mode_entry *, int); +static int window_copy_get_output_range(struct window_mode_entry *, int, + u_int *, u_int *, 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, + 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); +static void window_copy_pipe_buffer(struct session *, const char *, + void *, size_t); static void window_copy_pipe(struct window_mode_entry *, struct session *, const char *); static void window_copy_copy_pipe(struct window_mode_entry *, @@ -133,6 +141,8 @@ static void window_copy_copy_selection(struct window_mode_entry *, const char *, int, int); static void window_copy_append_selection(struct window_mode_entry *); static void window_copy_clear_selection(struct window_mode_entry *); +static u_int window_copy_copy_line_length(struct window_mode_entry *, + u_int, int *); static void window_copy_copy_line(struct window_mode_entry *, char **, size_t *, u_int, u_int, u_int); static int window_copy_in_set(struct window_mode_entry *, u_int, u_int, @@ -1155,7 +1165,9 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) format_add(ft, "selection_active", "1"); else format_add(ft, "selection_active", "0"); - if (data->endselx != data->selx || data->endsely != data->sely) + if (data->modekeys == MODEKEY_VI || + data->endselx != data->selx || + data->endsely != data->sely) format_add(ft, "selection_present", "1"); else format_add(ft, "selection_present", "0"); @@ -1605,6 +1617,84 @@ window_copy_cmd_copy_selection_and_cancel(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); } +static enum window_copy_cmd_action +window_copy_cmd_select_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; + void *buf; + size_t len; + u_int sx, sy, ex, ey, last; + int all = args_has(cs->wargs, 'a'); + int wrapped; + + if (!window_copy_get_output_range(wme, all, &sx, &sy, &ex, &ey)) + return (WINDOW_COPY_CMD_NOTHING); + + /* An empty output selects nothing. */ + buf = window_copy_get_grid_range(wme, sx, sy, ex, ey, &len); + if (buf == NULL) + return (WINDOW_COPY_CMD_NOTHING); + free(buf); + + 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_start_selection(wme); + + /* The end is inclusive in vi mode, so step back onto the last cell. */ + if (data->modekeys == MODEKEY_VI) { + last = window_copy_copy_line_length(wme, ey, NULL); + if (ex > last) + ex = last; + if (ex == 0 && ey > 0) { + ey--; + ex = window_copy_copy_line_length(wme, ey, &wrapped); + if (wrapped && ex > 0) + ex--; + } else { + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_left(&gr, 1); + grid_reader_get_cursor(&gr, &ex, &ey); + } + } + window_copy_scroll_to(wme, ex, ey, 1); + return (WINDOW_COPY_CMD_REDRAW); +} + +static enum window_copy_cmd_action +window_copy_cmd_copy_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *prefix = NULL; + const char *arg0 = args_string(cs->wargs, 0); + size_t len; + int all = args_has(cs->wargs, 'a'); + int set_paste = !args_has(cs->wargs, 'P'); + int set_clip = !args_has(cs->wargs, 'C'); + + if (arg0 != NULL) + prefix = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_copy_buffer(wme, prefix, buf, len, + set_paste, set_clip); + } + } + free(prefix); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_cursor_down(struct window_copy_cmd_state *cs) { @@ -2772,6 +2862,127 @@ window_copy_cmd_pipe_and_cancel(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); } +static enum window_copy_cmd_action +window_copy_cmd_copy_pipe_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *command = NULL, *prefix = NULL; + const char *arg0 = args_string(cs->wargs, 0); + const char *arg1 = args_string(cs->wargs, 1); + size_t len; + int all = args_has(cs->wargs, 'a'); + int set_paste = !args_has(cs->wargs, 'P'); + int set_clip = !args_has(cs->wargs, 'C'); + + if (arg1 != NULL) + prefix = format_single(NULL, arg1, c, s, wl, wp); + if (s != NULL && arg0 != NULL && *arg0 != '\0') + command = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_pipe_buffer(s, command, buf, len); + window_copy_copy_buffer(wme, prefix, buf, len, + set_paste, set_clip); + } + } + free(command); + free(prefix); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + +static enum window_copy_cmd_action +window_copy_cmd_pipe_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *command = NULL; + const char *arg0 = args_string(cs->wargs, 0); + size_t len; + int all = args_has(cs->wargs, 'a'); + + if (s != NULL && arg0 != NULL && *arg0 != '\0') + command = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_pipe_buffer(s, command, buf, len); + free(buf); + } + } + free(command); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + +static void +window_copy_open_done(char *buf, __unused size_t len, __unused void *arg) +{ + free(buf); +} + +static void +window_copy_open_buffer(struct window_copy_cmd_state *cs, void *buf, + size_t len) +{ + struct window_mode_entry *wme = cs->wme; + struct window_pane *wp = wme->wp; + char *editor = NULL; + const char *arg0 = args_string(cs->wargs, 0); + + if (arg0 != NULL && *arg0 != '\0') + editor = format_single(NULL, arg0, cs->c, cs->s, cs->wl, wp); + spawn_editor(cs->c, buf, len, editor, window_copy_open_done, NULL); + free(editor); +} + +static enum window_copy_cmd_action +window_copy_cmd_open_selection(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + void *buf; + size_t len; + + if (cs->c == NULL) + return (WINDOW_COPY_CMD_NOTHING); + if (data->screen.sel == NULL && data->lineflag == LINE_SEL_NONE) + return (WINDOW_COPY_CMD_NOTHING); + buf = window_copy_get_selection(wme, &len); + if (buf != NULL) { + window_copy_open_buffer(cs, buf, len); + free(buf); + } + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_open_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + void *buf; + size_t len; + + if (cs->c == NULL) + return (WINDOW_COPY_CMD_NOTHING); + buf = window_copy_get_output(wme, &len, args_has(cs->wargs, 'a')); + if (buf != NULL) { + window_copy_open_buffer(cs, buf, len); + free(buf); + } + return (WINDOW_COPY_CMD_NOTHING); +} + static enum window_copy_cmd_action window_copy_cmd_goto_line(struct window_copy_cmd_state *cs) { @@ -3426,6 +3637,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_copy_pipe_and_cancel }, + { .command = "copy-pipe-output", + .args = { "aCP", 0, 2, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_copy_pipe_output + }, + { .command = "copy-output", + .args = { "aCP", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_copy_output + }, { .command = "copy-selection-no-clear", .args = { "CP", 0, 1, NULL }, .flags = 0, @@ -3648,6 +3871,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, .f = window_copy_cmd_other_end }, + { .command = "open-output", + .args = { "a", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_open_output + }, + { .command = "open-selection", + .args = { "", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_open_selection + }, { .command = "page-down", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -3684,6 +3919,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_pipe_and_cancel }, + { .command = "pipe-output", + .args = { "a", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_pipe_output + }, + { .command = "pipe-selection", + .args = { "", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_pipe + }, { .command = "previous-matching-bracket", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -3861,6 +4108,12 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_search_reverse }, + { .command = "select-output", + .args = { "a", 0, 0, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_select_output + }, { .command = "select-line", .args = { "", 0, 0, NULL }, .flags = 0, @@ -6054,9 +6307,11 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, window_copy_redraw_lines(wme, cy, sy - cy + 1); } else { if (endsy < cy) { - window_copy_redraw_lines(wme, endsy, cy - endsy + 1); + window_copy_redraw_lines(wme, endsy, + cy - endsy + 1); } else { - window_copy_redraw_lines(wme, cy, endsy - cy + 1); + window_copy_redraw_lines(wme, cy, + endsy - cy + 1); } } } @@ -6064,6 +6319,78 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, return (1); } +/* Get the range of the current output, or the whole buffer with all. */ +static int +window_copy_get_output_range(struct window_mode_entry *wme, int all, + 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_reader gr; + u_int total, cursor_y; + int found; + + if (!all) { + cursor_y = screen_hsize(data->backing) + data->cy - data->oy; + grid_reader_start(&gr, gd, data->cx, cursor_y); + found = grid_reader_output_range(&gr, sx, sy, ex, ey); + return (found); + } + total = gd->hsize + gd->sy; + *sx = *sy = 0; + *ey = total - 1; + *ex = grid_get_line(gd, *ey)->cellused; + return (1); +} + +/* Get current output. */ +static void * +window_copy_get_output(struct window_mode_entry *wme, size_t *len, int all) +{ + void *buf; + u_int sx, sy, ex, ey; + + if (!window_copy_get_output_range(wme, all, &sx, &sy, &ex, &ey)) { + *len = 0; + return (NULL); + } + buf = window_copy_get_grid_range(wme, sx, sy, ex, ey, len); + return (buf); +} + +/* Get the text from sx,sy up to ex,ey, or NULL if there is none. */ +static void * +window_copy_get_grid_range(struct window_mode_entry *wme, 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, first, last; + + buf = xmalloc(1); + off = 0; + for (i = sy; i <= ey; i++) { + first = 0; + last = gd->sx; + if (i == sy) + first = sx; + if (i == ey) + last = ex; + window_copy_copy_line(wme, &buf, &off, i, first, last); + } + if (off != 0 && buf[off - 1] == '\n') + off--; + if (off == 0) { + free(buf); + *len = 0; + return (NULL); + } + *len = off; + return (buf); +} + static void * window_copy_get_selection(struct window_mode_entry *wme, size_t *len) { @@ -6074,7 +6401,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) size_t off; u_int i, xx, yy, sx, sy, ex, ey, ey_last; u_int firstsx, lastex, restex, restsx, selx; - int keys; + int keys, wrapped; if (data->screen.sel == NULL && data->lineflag == LINE_SEL_NONE) { buf = window_copy_match_at_cursor(data); @@ -6107,7 +6434,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) } /* Trim ex to end of line. */ - ey_last = window_copy_find_length(wme, ey); + ey_last = window_copy_copy_line_length(wme, ey, &wrapped); if (ex > ey_last) ex = ey_last; @@ -6178,8 +6505,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) } /* Remove final \n (unless at end in vi mode). */ if (keys == MODEKEY_EMACS || lastex <= ey_last) { - if (~grid_get_line(data->backing->grid, ey)->flags & - GRID_LINE_WRAPPED || lastex != ey_last) + if (!wrapped || lastex != ey_last) off -= 1; } *len = off; @@ -6220,18 +6546,28 @@ window_copy_pipe_run(struct window_mode_entry *wme, struct session *s, const char *cmd, size_t *len) { void *buf; - struct job *job; buf = window_copy_get_selection(wme, len); + window_copy_pipe_buffer(s, cmd, buf, *len); + return (buf); +} + +static void +window_copy_pipe_buffer(struct session *s, const char *cmd, void *buf, + size_t len) +{ + struct job *job; + + if (buf == NULL) + return; if (cmd == NULL || *cmd == '\0') cmd = options_get_string(global_options, "copy-command"); if (cmd != NULL && *cmd != '\0') { job = job_run(cmd, 0, NULL, NULL, s, NULL, NULL, NULL, NULL, NULL, JOB_NOWAIT, -1, -1); if (job != NULL) - bufferevent_write(job_get_event(job), buf, *len); + bufferevent_write(job_get_event(job), buf, len); } - return (buf); } static void @@ -6307,6 +6643,29 @@ window_copy_append_selection(struct window_mode_entry *wme) free(bufname); } +/* Get the length for copying, preserving spaces on wrapped lines. */ +static u_int +window_copy_copy_line_length(struct window_mode_entry *wme, u_int sy, + int *wrapped) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_line *gl = grid_get_line(gd, sy); + u_int length; + int line_wrapped; + + line_wrapped = 0; + if ((gl->flags & GRID_LINE_WRAPPED) && gl->cellsize <= gd->sx) + line_wrapped = 1; + if (wrapped != NULL) + *wrapped = line_wrapped; + if (line_wrapped) + length = gl->cellsize; + else + length = window_copy_find_length(wme, sy); + return (length); +} + static void window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, u_int sy, u_int sx, u_int ex) @@ -6314,27 +6673,15 @@ window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, struct window_copy_mode_data *data = wme->data; struct grid *gd = data->backing->grid; struct grid_cell gc; - struct grid_line *gl; struct utf8_data ud; - u_int i, xx, wrapped = 0; + u_int i, xx; const char *s; + int wrapped; if (sx > ex) return; - /* - * Work out if the line was wrapped at the screen edge and all of it is - * on screen. - */ - gl = grid_get_line(gd, sy); - if (gl->flags & GRID_LINE_WRAPPED && gl->cellsize <= gd->sx) - wrapped = 1; - - /* If the line was wrapped, don't strip spaces (use the full length). */ - if (wrapped) - xx = gl->cellsize; - else - xx = window_copy_find_length(wme, sy); + xx = window_copy_copy_line_length(wme, sy, &wrapped); if (ex > xx) ex = xx; if (sx > xx) diff --git a/window-customize.c b/window-customize.c index 1bf311ff2..b8ec4f140 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.36 2026/07/28 10:35:31 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.37 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -2011,13 +2011,15 @@ window_customize_edit_close_cb(char *buf, size_t len, void *arg) switch (ed->edit_type) { case WINDOW_CUSTOMIZE_EDIT_OPTION: if (window_customize_option_editable(data, item) && - window_customize_set_option_value(item, value, &cause) != 0) { + window_customize_set_option_value(item, value, + &cause) != 0) { free(cause); goto out; } break; case WINDOW_CUSTOMIZE_EDIT_KEY_COMMAND: - if (window_customize_set_command_value(item, value, &cause) != 0) { + if (window_customize_set_command_value(item, value, + &cause) != 0) { free(cause); goto out; } @@ -2104,8 +2106,8 @@ window_customize_start_edit(struct window_customize_modedata *data, buf = "\n"; len = 1; } - ed->editor = spawn_editor(c, buf, len, window_customize_edit_close_cb, - ed); + ed->editor = spawn_editor(c, buf, len, NULL, + window_customize_edit_close_cb, ed); free(value); if (ed->editor == NULL) window_customize_finish_edit(ed);