From 4dc67a5dd69bda46a2b066495d60fd04a6626c93 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 8 Oct 2026 06:29:32 +0000 Subject: [PATCH 1/4] Fix commands being lost when chained with assignment, GitHub issue 5702 from Jeong, Heon --- cmd-parse.y | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cmd-parse.y b/cmd-parse.y index 390ff627e..9f415c224 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.61 2026/10/05 13:45:49 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.62 2026/10/08 06:29:32 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -423,15 +423,12 @@ commands : command { struct cmd_parse_state *ps = &parse_state; + $$ = $1; if (!TAILQ_EMPTY(&$3->arguments) && (ps->scope == NULL || ps->scope->flag)) { - $$ = $1; TAILQ_INSERT_TAIL($$, $3, entry); - } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); + } else cmd_parse_free_command($3); - } } | condition1 { From 9a9229202957a63395cb0cef834b89bebe22982b Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 8 Oct 2026 06:44:16 +0000 Subject: [PATCH 2/4] Track allocations in yacc to avoid leaking when parsing commands, GitHub issue 5632 from Jeong, Heon. --- cmd-parse.y | 318 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 224 insertions(+), 94 deletions(-) diff --git a/cmd-parse.y b/cmd-parse.y index 9f415c224..95159e86c 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.62 2026/10/08 06:29:32 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.63 2026/10/08 06:44:16 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -69,6 +69,22 @@ struct cmd_parse_command { }; TAILQ_HEAD(cmd_parse_commands, cmd_parse_command); +enum cmd_parse_value_type { + CMD_PARSE_VALUE_STRING, + CMD_PARSE_VALUE_ARGUMENT, + CMD_PARSE_VALUE_ARGUMENTS, + CMD_PARSE_VALUE_COMMAND, + CMD_PARSE_VALUE_COMMANDS +}; + +struct cmd_parse_owned_value { + enum cmd_parse_value_type type; + void *value; + + TAILQ_ENTRY(cmd_parse_owned_value) entry; +}; +TAILQ_HEAD(cmd_parse_owned_values, cmd_parse_owned_value); + struct cmd_parse_state { FILE *f; @@ -87,10 +103,16 @@ struct cmd_parse_state { struct cmd_parse_scope *scope; TAILQ_HEAD(, cmd_parse_scope) stack; + + struct cmd_parse_owned_values values; }; static struct cmd_parse_state parse_state; static char *cmd_parse_get_error(const char *, u_int, const char *); +static void *cmd_parse_own(enum cmd_parse_value_type, void *); +static struct cmd_parse_commands *cmd_parse_own_commands(void); +static void *cmd_parse_take(void *); +static void cmd_parse_drop(void *); static void cmd_parse_free_command(struct cmd_parse_command *); static struct cmd_parse_commands *cmd_parse_new_commands(void); static void cmd_parse_free_commands(struct cmd_parse_commands *); @@ -139,7 +161,7 @@ lines : /* empty */ { struct cmd_parse_state *ps = &parse_state; - ps->commands = $1; + ps->commands = cmd_parse_take($1); } statements : statement '\n' @@ -150,18 +172,16 @@ statements : statement '\n' { $$ = $1; TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_drop($2); } statement : /* empty */ { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_own_commands(); } | hidden_assignment { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_own_commands(); } | condition { @@ -170,8 +190,8 @@ statement : /* empty */ if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($1); } } | commands @@ -181,8 +201,8 @@ statement : /* empty */ if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($1); } } @@ -204,6 +224,7 @@ expanded : format struct cmd_find_state *fsp; struct cmd_find_state fs; int flags = FORMAT_NOJOBS; + char *expanded; if (cmd_find_valid_state(&pi->fs)) fsp = &pi->fs; @@ -214,9 +235,10 @@ expanded : format ft = format_create(c, pi->item, FORMAT_NONE, flags); format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp); - $$ = format_expand(ft, $1); + expanded = format_expand(ft, $1); + $$ = cmd_parse_own(CMD_PARSE_VALUE_STRING, expanded); format_free(ft); - free($1); + cmd_parse_drop($1); } optional_assignment : /* empty */ @@ -241,7 +263,7 @@ assignment : EQUALS } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $1, 0); - free($1); + cmd_parse_drop($1); } hidden_assignment : HIDDEN EQUALS @@ -263,7 +285,7 @@ hidden_assignment : HIDDEN EQUALS } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $2, ENVIRON_HIDDEN); - free($2); + cmd_parse_drop($2); } if_open : IF expanded @@ -274,7 +296,7 @@ if_open : IF expanded scope = xmalloc(sizeof *scope); $$ = scope->flag = format_true($2); scope->taken = scope->flag; - free($2); + cmd_parse_drop($2); if (ps->scope != NULL) TAILQ_INSERT_HEAD(&ps->stack, ps->scope, entry); @@ -303,7 +325,7 @@ if_elif : ELIF expanded scope->flag = !ps->scope->taken && format_true($2); scope->taken = ps->scope->taken || scope->flag; $$ = scope->flag; - free($2); + cmd_parse_drop($2); free(ps->scope); ps->scope = scope; @@ -324,48 +346,48 @@ condition : if_open '\n' statements if_close if ($1) $$ = $3; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($3); } } | if_open '\n' statements if_else '\n' statements if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($6); + cmd_parse_drop($6); } else { $$ = $6; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } } | if_open '\n' statements elif if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($4.commands); + cmd_parse_drop($4.commands); } else if ($4.flag) { $$ = $4.commands; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } | if_open '\n' statements elif if_else '\n' statements if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($4.commands); - cmd_parse_free_commands($7); + cmd_parse_drop($4.commands); + cmd_parse_drop($7); } else if ($4.flag) { $$ = $4.commands; - cmd_parse_free_commands($3); - cmd_parse_free_commands($7); + cmd_parse_drop($3); + cmd_parse_drop($7); } else { $$ = $7; - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } @@ -376,8 +398,8 @@ elif : if_elif '\n' statements $$.commands = $3; } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($3); } } | if_elif '\n' statements elif @@ -385,16 +407,16 @@ elif : if_elif '\n' statements if ($1) { $$.flag = 1; $$.commands = $3; - cmd_parse_free_commands($4.commands); + cmd_parse_drop($4.commands); } else if ($4.flag) { $$.flag = 1; $$.commands = $4.commands; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } @@ -402,12 +424,13 @@ commands : command { struct cmd_parse_state *ps = &parse_state; - $$ = cmd_parse_new_commands(); + $$ = cmd_parse_own_commands(); if (!TAILQ_EMPTY(&$1->arguments) && - (ps->scope == NULL || ps->scope->flag)) + (ps->scope == NULL || ps->scope->flag)) { + cmd_parse_take($1); TAILQ_INSERT_TAIL($$, $1, entry); - else - cmd_parse_free_command($1); + } else + cmd_parse_drop($1); } | commands ';' { @@ -417,7 +440,7 @@ commands : command { $$ = $1; TAILQ_CONCAT($$, $3, entry); - free($3); + cmd_parse_drop($3); } | commands ';' command { @@ -426,9 +449,10 @@ commands : command $$ = $1; if (!TAILQ_EMPTY(&$3->arguments) && (ps->scope == NULL || ps->scope->flag)) { + cmd_parse_take($3); TAILQ_INSERT_TAIL($$, $3, entry); } else - cmd_parse_free_command($3); + cmd_parse_drop($3); } | condition1 { @@ -442,6 +466,7 @@ command : assignment $$ = xcalloc(1, sizeof *$$); $$->line = ps->input->line; TAILQ_INIT(&$$->arguments); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } | optional_assignment TOKEN { @@ -454,8 +479,9 @@ command : assignment arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; - arg->string = $2; + arg->string = cmd_parse_take($2); TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } | optional_assignment TOKEN arguments { @@ -467,12 +493,13 @@ command : assignment TAILQ_INIT(&$$->arguments); TAILQ_CONCAT(&$$->arguments, $3, entry); - free($3); + cmd_parse_drop($3); arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; - arg->string = $2; + arg->string = cmd_parse_take($2); TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } condition1 : if_open commands if_close @@ -480,48 +507,48 @@ condition1 : if_open commands if_close if ($1) $$ = $2; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($2); } } | if_open commands if_else commands if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($4); + cmd_parse_drop($4); } else { $$ = $4; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } } | if_open commands elif1 if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($3.commands); + cmd_parse_drop($3.commands); } else if ($3.flag) { $$ = $3.commands; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } | if_open commands elif1 if_else commands if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($3.commands); - cmd_parse_free_commands($5); + cmd_parse_drop($3.commands); + cmd_parse_drop($5); } else if ($3.flag) { $$ = $3.commands; - cmd_parse_free_commands($2); - cmd_parse_free_commands($5); + cmd_parse_drop($2); + cmd_parse_drop($5); } else { $$ = $5; - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } @@ -532,8 +559,8 @@ elif1 : if_elif commands $$.commands = $2; } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($2); } } | if_elif commands elif1 @@ -541,16 +568,16 @@ elif1 : if_elif commands if ($1) { $$.flag = 1; $$.commands = $2; - cmd_parse_free_commands($3.commands); + cmd_parse_drop($3.commands); } else if ($3.flag) { $$.flag = 1; $$.commands = $3.commands; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } @@ -559,10 +586,13 @@ arguments : argument $$ = xcalloc(1, sizeof *$$); TAILQ_INIT($$); + cmd_parse_take($1); TAILQ_INSERT_HEAD($$, $1, entry); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENTS, $$); } | argument arguments { + cmd_parse_take($1); TAILQ_INSERT_HEAD($2, $1, entry); $$ = $2; } @@ -571,19 +601,22 @@ argument : TOKEN { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$->string = cmd_parse_take($1); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } | EQUALS { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$->string = cmd_parse_take($1); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } | '{' argument_statements { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_COMMANDS; - $$->commands = $2; + $$->commands = cmd_parse_take($2); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } argument_statements : statement '}' @@ -594,7 +627,7 @@ argument_statements : statement '}' { $$ = $1; TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_drop($2); } %% @@ -683,22 +716,116 @@ cmd_parse_free_commands(struct cmd_parse_commands *cmds) free(cmds); } +static void * +cmd_parse_own(enum cmd_parse_value_type type, void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = xmalloc(sizeof *v); + v->type = type; + v->value = value; + TAILQ_INSERT_TAIL(&ps->values, v, entry); + return (value); +} + +static struct cmd_parse_commands * +cmd_parse_own_commands(void) +{ + struct cmd_parse_commands *cmds; + + cmds = cmd_parse_new_commands(); + return (cmd_parse_own(CMD_PARSE_VALUE_COMMANDS, cmds)); +} + +static struct cmd_parse_owned_value * +cmd_parse_find_value(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + TAILQ_FOREACH_REVERSE(v, &ps->values, cmd_parse_owned_values, entry) { + if (v->value == value) + return (v); + } + fatalx("parser value not owned"); +} + +static void * +cmd_parse_take(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = cmd_parse_find_value(value); + TAILQ_REMOVE(&ps->values, v, entry); + free(v); + return (value); +} + +static void +cmd_parse_free_value(enum cmd_parse_value_type type, void *value) +{ + switch (type) { + case CMD_PARSE_VALUE_STRING: + free(value); + break; + case CMD_PARSE_VALUE_ARGUMENT: + cmd_parse_free_argument(value); + break; + case CMD_PARSE_VALUE_ARGUMENTS: + cmd_parse_free_arguments(value); + free(value); + break; + case CMD_PARSE_VALUE_COMMAND: + cmd_parse_free_command(value); + break; + case CMD_PARSE_VALUE_COMMANDS: + cmd_parse_free_commands(value); + break; + } +} + +static void +cmd_parse_drop(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = cmd_parse_find_value(value); + TAILQ_REMOVE(&ps->values, v, entry); + cmd_parse_free_value(v->type, v->value); + free(v); +} + static struct cmd_parse_commands * cmd_parse_run_parser(char **cause) { - struct cmd_parse_state *ps = &parse_state; - struct cmd_parse_scope *scope, *scope1; - int retval; + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_scope *scope, *scope1; + struct cmd_parse_owned_value *v, *v1; + int retval; ps->commands = NULL; TAILQ_INIT(&ps->stack); + TAILQ_INIT(&ps->values); retval = yyparse(); TAILQ_FOREACH_SAFE(scope, &ps->stack, entry, scope1) { TAILQ_REMOVE(&ps->stack, scope, entry); free(scope); } + free(ps->scope); + ps->scope = NULL; + + TAILQ_FOREACH_SAFE(v, &ps->values, entry, v1) { + TAILQ_REMOVE(&ps->values, v, entry); + cmd_parse_free_value(v->type, v->value); + free(v); + } if (retval != 0) { + if (ps->commands != NULL) + cmd_parse_free_commands(ps->commands); *cause = ps->error; return (NULL); } @@ -1329,9 +1456,10 @@ yylex(void) */ next = yylex_getc(); if (condition && next == '{') { - yylval.token = yylex_format(); - if (yylval.token == NULL) + token = yylval.token = yylex_format(); + if (token == NULL) return (ERROR); + cmd_parse_own(CMD_PARSE_VALUE_STRING, token); return (FORMAT); } while (next != '\n' && next != EOF) @@ -1348,35 +1476,37 @@ yylex(void) * % is a condition unless it is all % or all numbers, * then it is a token. */ - yylval.token = yylex_get_word('%'); - for (cp = yylval.token; *cp != '\0'; cp++) { + token = yylval.token = yylex_get_word('%'); + for (cp = token; *cp != '\0'; cp++) { if (*cp != '%' && !isdigit((u_char)*cp)) break; } - if (*cp == '\0') + if (*cp == '\0') { + cmd_parse_own(CMD_PARSE_VALUE_STRING, token); return (TOKEN); + } ps->condition = 1; - if (strcmp(yylval.token, "%hidden") == 0) { - free(yylval.token); + if (strcmp(token, "%hidden") == 0) { + free(token); return (HIDDEN); } - if (strcmp(yylval.token, "%if") == 0) { - free(yylval.token); + if (strcmp(token, "%if") == 0) { + free(token); return (IF); } - if (strcmp(yylval.token, "%else") == 0) { - free(yylval.token); + if (strcmp(token, "%else") == 0) { + free(token); return (ELSE); } - if (strcmp(yylval.token, "%elif") == 0) { - free(yylval.token); + if (strcmp(token, "%elif") == 0) { + free(token); return (ELIF); } - if (strcmp(yylval.token, "%endif") == 0) { - free(yylval.token); + if (strcmp(token, "%endif") == 0) { + free(token); return (ENDIF); } - free(yylval.token); + free(token); return (ERROR); } @@ -1386,7 +1516,7 @@ yylex(void) token = yylex_token(ch); if (token == NULL) return (ERROR); - yylval.token = token; + yylval.token = cmd_parse_own(CMD_PARSE_VALUE_STRING, token); if (strchr(token, '=') != NULL && yylex_is_var(*token, 1)) { for (cp = token + 1; *cp != '='; cp++) { From cb44afd41522ce058b71d76e9deb433d7d0a694c Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 8 Oct 2026 07:06:26 +0000 Subject: [PATCH 3/4] Fix bugs with empty panes and load-buffer -w, GitHub issue 5701 from Alexandre Fiori. --- cmd-load-buffer.c | 6 +++--- spawn.c | 4 +++- window.c | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index df35b82c1..e9b016071 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-load-buffer.c,v 1.66 2025/10/28 07:32:26 nicm Exp $ */ +/* $OpenBSD: cmd-load-buffer.c,v 1.67 2026/10/08 07:06:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -78,9 +78,9 @@ cmd_load_buffer_done(__unused struct client *c, const char *path, int error, tc->session != NULL && (~tc->flags & CLIENT_DEAD)) tty_set_selection(&tc->tty, "", copy, bsize); - if (tc != NULL) - server_client_unref(tc); } + if (tc != NULL) + server_client_unref(tc); cmdq_continue(item); free(cdata->name); diff --git a/spawn.c b/spawn.c index b0d3aedee..2a0c73a41 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.55 2026/10/07 12:55:41 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.56 2026/10/08 07:06:26 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -462,6 +462,8 @@ spawn_pane(struct spawn_context *sc, char **cause) /* If the command is empty, don't fork a child process. */ if (sc->flags & SPAWN_EMPTY) { new_wp->flags |= PANE_EMPTY; + new_wp->pid = 0; + *new_wp->tty = '\0'; new_wp->base.mode &= ~MODE_CURSOR; new_wp->base.mode |= MODE_CRLF; goto complete; diff --git a/window.c b/window.c index 662871313..0b280f19a 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.384 2026/10/02 12:48:52 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.385 2026/10/08 07:06:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1585,9 +1585,11 @@ window_pane_destroy(struct window_pane *wp) window_pane_free_modes(wp); screen_write_sync_clear_dirty(wp); - if (wp->fd != -1) { + if (wp->event != NULL) { bufferevent_free(wp->event); wp->event = NULL; + } + if (wp->fd != -1) { close(wp->fd); wp->fd = -1; } From a0a0e12234d1e50b279d54ded30f926960896075 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 8 Oct 2026 07:50:05 +0000 Subject: [PATCH 4/4] Add commands to copy mode to select, pipe and open OSC 133 command output, from Michael Grant. --- grid-reader.c | 207 ++++++++++++++++++++++- grid.c | 81 ++++++++- input.c | 3 +- key-bindings.c | 7 +- spawn.c | 8 +- tmux.1 | 70 +++++++- tmux.h | 6 +- window-buffer.c | 5 +- window-copy.c | 399 ++++++++++++++++++++++++++++++++++++++++++--- window-customize.c | 12 +- 10 files changed, 744 insertions(+), 54 deletions(-) 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);