diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index e6765da99..902783e08 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -17,8 +17,9 @@ jobs: action: runs-on: ubuntu-latest if: github.repository == 'tmux/tmux' + timeout-minutes: 10 steps: - - uses: dessant/lock-threads@v6 + - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index a84478357..b1e19e9e0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -38,7 +38,9 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: dependencies if: runner.os == 'Linux' @@ -81,7 +83,7 @@ jobs: - name: logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: regress-logs-${{ matrix.name }} path: regress/logs/*.log diff --git a/arguments.c b/arguments.c index a4b6a0f35..00af96311 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ +/* $OpenBSD: arguments.c,v 1.67 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx, cmdq_error(item, "%s", error); free(error); } - else - cmdlist->references++; args_make_commands_free(state); return (cmdlist); } @@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv, int i; if (state->cmdlist != NULL) { - if (argc == 0) + if (argc == 0) { + state->cmdlist->references++; return (state->cmdlist); + } return (cmd_list_copy(state->cmdlist, argc, argv)); } diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index ac1117347..1d135b834 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s, } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } cmd_free_argv(argc, argv); diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 738fb3975..ef369e1ba 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -412,7 +412,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) server_kill_pane(w->modal); return (CMD_RETURN_NORMAL); } - + if (tc->flags & CLIENT_CONTROL) + return (CMD_RETURN_NORMAL); if (w->modal != NULL) return (CMD_RETURN_NORMAL); diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 49b92a996..59d34f9d6 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ +/* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); return (CMD_RETURN_NORMAL); } @@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } out: diff --git a/cmd-parse.y b/cmd-parse.y index 29b7780e0..4a6ec66b1 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.59 2026/08/31 07:51:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, if (last == NULL) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); + cmd_parse_free_commands(cmds); return (1); } @@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, pi->flags |= CMD_PARSE_NOALIAS; cmd_parse_build_commands(cmds, pi, pr); pi->flags &= ~CMD_PARSE_NOALIAS; + cmd_parse_free_commands(cmds); return (1); } diff --git a/cmd-queue.c b/cmd-queue.c index 35373a32a..a9ce469bc 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -850,7 +850,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...) if (c == NULL) { cmd_get_source(cmd, &file, &line); - cfg_add_cause("%s:%u: %s", file, line, msg); + if (!cfg_finished) { + if (file != NULL) + cfg_add_cause("%s:%u: %s", file, line, msg); + else + cfg_add_cause("%s", msg); + } else { + if (file != NULL) { + server_add_message("message: %s:%u: %s", file, + line, msg); + } else + server_add_message("message: %s", msg); + } } else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { server_add_message("%s message: %s", c->name, msg); if (~c->flags & CLIENT_UTF8) { diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 01ddf424a..fc5062ab4 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -92,6 +92,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } server_unzoom_window(w); + lc = wp->layout_cell; /* may have been replaced by unzoom */ if (args_has(args, 'x')) { x = args_percentage(args, 'x', 0, PANE_MAXIMUM, w->sx, &cause); diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 59d5aeea6..7e0c62e56 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -233,9 +233,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } if (cdata->item != NULL) diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 6e640fec6..3e540f706 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "%s", cause); goto fail; } + free(bufname); return (CMD_RETURN_NORMAL); } @@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "no data specified"); goto fail; } - if ((newsize = strlen(args_string(args, 0))) == 0) + if ((newsize = strlen(args_string(args, 0))) == 0) { + free(bufname); return (CMD_RETURN_NORMAL); + } if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); @@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); + free(bufname); return (CMD_RETURN_NORMAL); fail: diff --git a/format.c b/format.c index d88c29d66..78e3f0fb5 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1887,6 +1887,37 @@ format_cb_cursor_blinking(struct format_tree *ft) return (NULL); } +/* Callback for history_added. */ +static void * +format_cb_history_added(struct format_tree *ft) +{ + if (ft->wp != NULL) + return (format_printf("%u", ft->wp->base.grid->scroll_added)); + return (NULL); +} + +/* Callback for history_collected. */ +static void * +format_cb_history_collected(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_collected)); + return (NULL); +} + +/* Callback for history_generation. */ +static void * +format_cb_history_generation(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_generation)); + return (NULL); +} + /* Callback for history_limit. */ static void * format_cb_history_limit(struct format_tree *ft) @@ -2276,6 +2307,19 @@ format_cb_pane_last_output_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_output_generation. */ +static void * +format_cb_pane_output_generation(struct format_tree *ft) +{ + unsigned long long value; + + if (ft->wp != NULL) { + value = ft->wp->output_generation; + return (format_printf("%llu", value)); + } + return (NULL); +} + /* Callback for pane_last_prompt_time. */ static void * format_cb_pane_last_prompt_time(struct format_tree *ft) @@ -3663,12 +3707,21 @@ static const struct format_table_entry format_table[] = { { "cursor_y", FORMAT_TABLE_STRING, format_cb_cursor_y }, + { "history_added", FORMAT_TABLE_STRING, + format_cb_history_added + }, { "history_all_bytes", FORMAT_TABLE_STRING, format_cb_history_all_bytes }, { "history_bytes", FORMAT_TABLE_STRING, format_cb_history_bytes }, + { "history_collected", FORMAT_TABLE_STRING, + format_cb_history_collected + }, + { "history_generation", FORMAT_TABLE_STRING, + format_cb_history_generation + }, { "history_limit", FORMAT_TABLE_STRING, format_cb_history_limit }, @@ -3849,6 +3902,9 @@ static const struct format_table_entry format_table[] = { { "pane_mode", FORMAT_TABLE_STRING, format_cb_pane_mode }, + { "pane_output_generation", FORMAT_TABLE_STRING, + format_cb_pane_output_generation + }, { "pane_path", FORMAT_TABLE_STRING, format_cb_pane_path }, @@ -4387,7 +4443,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL) + if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } diff --git a/grid.c b/grid.c index 53c8c2cb0..424f5bea3 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1635,7 +1635,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy) void grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) { - u_int yy, ay = 0; + u_int yy, ay = 0, ey = gd->hsize + gd->sy - 1; for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) { if (ay == wy) @@ -1649,7 +1649,7 @@ grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) * until we find the end or the line now containing wx. */ if (wx == UINT_MAX) { - while (gd->linedata[yy].flags & GRID_LINE_WRAPPED) + while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED) yy++; wx = gd->linedata[yy].cellused; } else { @@ -1711,21 +1711,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) { struct grid_cell gc, tmp_gc; u_int pxx; + int has_tab, has_space; + + has_tab = (strchr(set, '\t') != NULL); + has_space = (strchr(set, ' ') != NULL); grid_get_cell(gd, px, py, &gc); - if (strchr(set, '\t')) { - if (gc.flags & GRID_FLAG_PADDING) { - pxx = px; - do - grid_get_cell(gd, --pxx, py, &tmp_gc); - while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); - if (tmp_gc.flags & GRID_FLAG_TAB) - return (tmp_gc.data.width - (px - pxx)); - } else if (gc.flags & GRID_FLAG_TAB) - return (gc.data.width); - } - if (gc.flags & GRID_FLAG_PADDING) + if (gc.flags & GRID_FLAG_PADDING) { + if (!has_tab && !has_space) + return (0); + pxx = px; + do + grid_get_cell(gd, --pxx, py, &tmp_gc); + while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); + if (((has_tab || has_space) && + (tmp_gc.flags & GRID_FLAG_TAB)) || + (has_space && utf8_has_whitespace(&tmp_gc.data))) + return (tmp_gc.data.width - (px - pxx)); return (0); + } + if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB)) + return (gc.data.width); + if (has_space && utf8_has_whitespace(&gc.data)) + return (gc.data.width == 0 ? 1 : gc.data.width); return (utf8_cstrhas(set, &gc.data)); } diff --git a/image.c b/image.c index f0543dc4f..5613cf920 100644 --- a/image.c +++ b/image.c @@ -84,6 +84,11 @@ image_fallback(char **ret, u_int sx, u_int sy) char *buf, *label; u_int py, size, lsize; + if (sy == 0) { + *ret = xstrdup(""); + return; + } + /* Allocate first line. */ lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; if (sx < lsize - 3) diff --git a/input.c b/input.c index 383f83998..3b3abaaa2 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1047,6 +1047,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) if (len == 0) return; + wp->output_generation++; window_update_activity(wp->window); if (~wp->flags & PANE_ACTIVITY) { wp->flags |= PANE_ACTIVITY; diff --git a/layout.c b/layout.c index 02a4b681a..ac87f30e5 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -539,6 +539,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc, status = window_get_pane_status(w); + /* Floating cells do not take space from the tiled layout. */ + if (!layout_cell_is_tiled(lc) && !layout_cell_has_tiled_child(lc)) + return (0); + if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { @@ -568,6 +572,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc, /* Different type: minimum of available space in child cells. */ minimum = UINT_MAX; TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (!layout_cell_is_tiled(lcchild) && + !layout_cell_has_tiled_child(lcchild)) + continue; available = layout_resize_check(w, lcchild, type); if (available < minimum) minimum = available; @@ -661,7 +668,7 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc, /* Find and return the nearest neighbour to a cell in a specific direction. */ static struct layout_cell * -layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) +layout_cell_get_neighbour_dir(struct layout_cell *lc, int direction) { struct layout_cell *lcn = lc; @@ -695,9 +702,9 @@ layout_cell_get_neighbour(struct layout_cell *lc) if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) direction = !direction; - lcother = layout_cell_get_neighbour_direction(lc, direction); + lcother = layout_cell_get_neighbour_dir(lc, direction); if (lcother == NULL) - lcother = layout_cell_get_neighbour_direction(lc, !direction); + lcother = layout_cell_get_neighbour_dir(lc, !direction); return (lcother); } @@ -860,7 +867,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type, size = lc->g.sx; else size = lc->g.sy; - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) + if (layout_cell_is_last_tiled(lc)) change = size - new_size; else change = new_size - size; @@ -985,11 +992,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change, if (lcparent == NULL) return; - /* If this is the last cell, move back one. */ - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) { - do - lc = TAILQ_PREV(lc, layout_cells, entry); - while (lc->flags & LAYOUT_CELL_FLOATING); + /* If this is the last tiled cell, move back one. */ + if (layout_cell_is_last_tiled(lc)) { + lc = layout_cell_get_neighbour_dir(lc, 0); + if (lc == NULL) + return; } layout_resize_layout(wp->window, lc, type, change, opposite); @@ -1007,22 +1014,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc, lcadd = lc; /* Look towards the tail for a suitable cell for reduction. */ - lcremove = TAILQ_NEXT(lc, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 1); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_NEXT(lcremove, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 1); } /* If none found, look towards the head. */ if (opposite && lcremove == NULL) { - lcremove = TAILQ_PREV(lc, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 0); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } } if (lcremove == NULL) @@ -1050,13 +1057,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc, size = layout_resize_check(w, lcremove, type); if (size != 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } while (lcremove != NULL); if (lcremove == NULL) return (0); /* And add onto the next cell (from the original cell). */ - lcadd = TAILQ_NEXT(lc, entry); + lcadd = layout_cell_get_neighbour_dir(lc, 1); if (lcadd == NULL) return (0); diff --git a/options-table.c b/options-table.c index 1a1702076..32f842347 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -305,6 +305,16 @@ const struct options_table_entry options_table[] = { "When this is reached, the oldest buffer is deleted." }, + { .name = "clear-on-attach", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether to use the alternate screen and clear it when " + "a client is attached. When disabled, tmux does not " + "enter the alternate screen on attach so terminal " + "content before tmux remains in scrollback." + }, + { .name = "command-alias", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_SERVER, @@ -1240,7 +1250,8 @@ const struct options_table_entry options_table[] = { * underscore. */ .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", - .text = "Characters considered to separate words." + .text = "Characters considered to separate words; a space matches " + "any character with the Unicode White_Space property." }, /* Window options. */ diff --git a/regress/buffers.sh b/regress/buffers.sh index fdacacb5b..18ae4439c 100644 --- a/regress/buffers.sh +++ b/regress/buffers.sh @@ -28,7 +28,7 @@ $TMUX kill-server 2>/dev/null TMP=$(mktemp) TMP2=$(mktemp) -trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 +trap 'rm -f "$TMP" "$TMP2" "$TMP".*; $TMUX kill-server 2>/dev/null' 0 1 15 # check_ok $cmd... # @@ -270,6 +270,27 @@ if [ $? -eq 0 ]; then exit 1 fi +# A write error after a successful open must be reported to the server. +dd if=/dev/zero of="$TMP.big" bs=100000 count=1 2>/dev/null +check_ok load-buffer -b write-error "$TMP.big" +mkfifo "$TMP.fifo" || exit 1 +dd if="$TMP.fifo" of="$TMP.got" bs=1 count=8 2>/dev/null & +reader=$! +exec 3>"$TMP.fifo" +out=$($TMUX save-buffer -b write-error "$TMP.fifo" 3>&- 2>&1) +status=$? +exec 3>&- +wait "$reader" +if [ $status -eq 0 ]; then + echo "save-buffer after write error succeeded." + exit 1 +fi +if [ "$(wc -c <"$TMP.got")" -ne 8 ]; then + echo "save-buffer write error test wrote wrong amount." + exit 1 +fi +check_ok delete-buffer -b write-error + # save-buffer - writes to stdout and load-buffer - reads from stdin. out=$($TMUX save-buffer -b sb -) if [ "$out" != "data" ]; then diff --git a/regress/copy-mode-unicode-whitespace.sh b/regress/copy-mode-unicode-whitespace.sh new file mode 100755 index 000000000..a532606a8 --- /dev/null +++ b/regress/copy-mode-unicode-whitespace.sh @@ -0,0 +1,163 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +fail() +{ + echo "$1" + [ -z "$TMUX" ] || $TMUX kill-server 2>/dev/null + exit 1 +} + +goto_cell() +{ + row=$1 + column=$2 + + $TMUX send-keys -X history-top || fail "$mode: history-top failed" + $TMUX send-keys -X start-of-line || fail "$mode: start-of-line failed" + [ "$row" -eq 0 ] || + $TMUX send-keys -X -N "$row" cursor-down || + fail "$mode: cursor-down failed" + [ "$column" -eq 0 ] || + $TMUX send-keys -X -N "$column" cursor-right || + fail "$mode: cursor-right failed" +} + +check_cursor() +{ + expected=$1 + actual=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}') + [ "$actual" = "$expected" ] || + fail "$mode: expected cursor $expected, got $actual" +} + +check_word() +{ + row=$1 + column=$2 + expected=$3 + + goto_cell "$row" "$column" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected word '$expected', got '$actual'" +} + +check_next_word() +{ + row=$1 + expected=$2 + + goto_cell "$row" 0 + $TMUX send-keys -X next-word || fail "$mode: next-word failed" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected next word '$expected', got '$actual'" +} + +for mode in emacs vi; do + TMUX="$TEST_TMUX -Lunicode-whitespace-$mode-$$ -f/dev/null" + $TMUX kill-server 2>/dev/null + $TMUX new-session -d -x16 -y20 \ + "printf 'aa bb\naa\tbb\naa\302\240bb\naa\343\200\200bb\naa:bb\naaaaaaaaaaaaaaa\302\240b\nabcdefghijklmnopq\nhardone\nhardtwo\naa\341\232\200bb\naa\342\200\200bb\naa\342\200\250bb\naa\342\200\251bb\naa\342\200\257bb\naa\342\201\237bb\nxa\302\205b c\n'; exec cat" || + fail "$mode: new-session failed" + $TMUX set-option -g window-size manual || fail "$mode: set size failed" + $TMUX set-window-option -g mode-keys "$mode" || + fail "$mode: set mode-keys failed" + $TMUX set-window-option -g word-separators "" || + fail "$mode: clear word-separators failed" + $TMUX copy-mode || fail "$mode: copy-mode failed" + + # ASCII space and TAB retain their existing behaviour. + goto_cell 0 0 + $TMUX send-keys -X next-space || fail "$mode: ASCII next-space failed" + check_cursor 3,0 + goto_cell 1 0 + $TMUX send-keys -X next-space || fail "$mode: TAB next-space failed" + check_cursor 8,1 + $TMUX send-keys -X previous-space || + fail "$mode: TAB previous-space failed" + check_cursor 0,1 + + # NBSP is whitespace for selection and word movement. + goto_cell 2 0 + $TMUX send-keys -X next-word || fail "$mode: NBSP next-word failed" + check_cursor 3,2 + $TMUX send-keys -X previous-word || + fail "$mode: NBSP previous-word failed" + check_cursor 0,2 + check_word 2 0 aa + goto_cell 2 0 + $TMUX send-keys -X next-word-end || + fail "$mode: NBSP next-word-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,2 + else + check_cursor 1,2 + fi + + # U+3000 is two columns wide; both its leading and padding cells must be + # skipped as one whitespace character in either direction. + goto_cell 3 0 + $TMUX send-keys -X next-word || fail "$mode: U+3000 next-word failed" + check_cursor 4,3 + $TMUX send-keys -X previous-word || + fail "$mode: U+3000 previous-word failed" + check_cursor 0,3 + goto_cell 3 0 + $TMUX send-keys -X next-space-end || + fail "$mode: U+3000 next-space-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,3 + else + check_cursor 1,3 + fi + + # Other characters in word-separators remain literal when space enables + # Unicode whitespace matching. + $TMUX set-window-option -g word-separators ': ' || + fail "$mode: set custom word-separators failed" + goto_cell 4 0 + $TMUX send-keys -X next-word || fail "$mode: literal next-word failed" + check_cursor 2,4 + $TMUX set-window-option -g word-separators "" || + fail "$mode: restore word-separators failed" + + # Whitespace in the last column remains a boundary across a soft wrap. + goto_cell 5 0 + $TMUX send-keys -X next-space || + fail "$mode: wrapped NBSP next-space failed" + check_cursor 0,6 + + # A word split only by a soft wrap remains one word, while a hard line + # boundary still terminates selection. + check_word 7 0 abcdefghijklmnopq + check_word 9 0 hardone + + # Check the remaining representative White_Space ranges and U+0085 + # combined into a cell with another character. + check_next_word 11 bb + check_next_word 12 bb + check_next_word 13 bb + check_next_word 14 bb + check_next_word 15 bb + check_next_word 16 bb + goto_cell 17 0 + $TMUX send-keys -X next-word || + fail "$mode: combined U+0085 next-word failed" + check_cursor 2,17 + + $TMUX kill-server 2>/dev/null +done + +exit 0 diff --git a/regress/floating-pane-geometry.sh b/regress/floating-pane-geometry.sh index 7e80e8942..b04344ff2 100644 --- a/regress/floating-pane-geometry.sh +++ b/regress/floating-pane-geometry.sh @@ -199,5 +199,74 @@ must_fail $TMUX resize-pane -t "$id" -x 0 must_fail $TMUX resize-pane -t "$id" -y 0 $TMUX kill-pane -t "$id" || exit 1 +# --- Tiled pane resize with floating cells in the layout --- + +$TMUX set-option -w -u pane-border-lines || exit 1 +base=$($TMUX display-message -p '#{pane_id}') + +# A floating cell after the last tiled cell must not become the recipient of a +# relative or absolute resize. This is the sequence from GitHub issue 5135. +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 50% -y 50% -X 50% -Y 50% 'sleep 100') || + fail "new-pane for tiled resize test failed" +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for tiled resize test failed" + +$TMUX resize-pane -t "$lower" -U 5 || fail "relative tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 13 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX resize-pane -t "$lower" -y 11 || fail "absolute tiled reset failed" +$TMUX resize-pane -t "$lower" -y 16 || fail "absolute tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A floating cell between tiled siblings must be skipped when finding the cell +# which donates space to a resize. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for middle floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 20 -y 8 -X 30 -Y 8 'sleep 100') || + fail "new-pane for middle floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "resize past floating cell failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 6 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A small floating child must not limit the available tiled space in a nested +# layout with a different split direction. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for nested floating cell test failed" +right=$($TMUX split-window -dhPF '#{pane_id}' -t "$lower" 'sleep 100') || + fail "horizontal split for nested floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$lower" \ + -x 20 -y 3 -X 30 -Y 10 'sleep 100') || + fail "new-pane for nested floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "nested tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 1 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$right" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + $TMUX kill-server 2>/dev/null exit 0 diff --git a/screen-write.c b/screen-write.c index 41d7e5248..7f7db74af 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.288 2026/08/21 09:53:04 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -305,11 +305,13 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, if (~ctx->flags & SCREEN_WRITE_SYNC) { /* - * For the active pane, only use synchronized updates if - * requested (commands that move the cursor); for other panes, - * always use it, since the cursor will have to move. + * For the active pane showing its base screen, only use + * synchronized updates if requested (commands that move the + * cursor); for other panes or a pane in a mode, always use it, + * since the cursor will have to move. */ - if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active) + if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active || + ctx->wp->screen != &ctx->wp->base)) ttyctx->flags |= TTY_CTX_SYNC; else if (is_sync) ttyctx->flags |= TTY_CTX_SYNC; @@ -333,10 +335,18 @@ screen_write_make_list(struct screen *s) void screen_write_free_list(struct screen *s) { - u_int y; + struct screen_write_cline *cl; + struct screen_write_citem *ci, *ci1; + u_int y; - for (y = 0; y < screen_size_y(s); y++) - free(s->write_list[y].data); + for (y = 0; y < screen_size_y(s); y++) { + cl = &s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + free(cl->data); + } free(s->write_list); } diff --git a/server-client.c b/server-client.c index 9d5954c61..23379c41d 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.505 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -406,7 +406,10 @@ server_client_lost(struct client *c) input_cancel_requests(c); free(c->title); + free(c->path); free((void *)c->cwd); + free(c->exit_session); + free(c->exit_message); evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); @@ -1378,6 +1381,10 @@ server_client_key_callback(struct cmdq_item *item, void *data) TAILQ_EMPTY(&wp->modes)) goto forward_key; + /* Focus events are not keys and cannot be bound. */ + if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT) + goto forward_key; + /* * Work out the current key table. If the pane is in a mode, use * the mode table instead of the default key table. @@ -2269,8 +2276,6 @@ server_client_check_exit(struct client *c, int force) proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); break; } - free(c->exit_session); - free(c->exit_message); } /* Redraw timer callback. */ @@ -2345,7 +2350,7 @@ server_client_check_redraw(struct client *c) /* Work out if a redraw is actually needed. */ needed = 0; - if (c->flags & CLIENT_ALLREDRAWFLAGS) + if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS)) needed = 1; else if (server_client_any_pane_redraw(c)) needed = 1; @@ -2371,8 +2376,14 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } - if (server_client_any_pane_redraw(c)) - c->flags |= CLIENT_REDRAWWINDOW; + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_REDRAW) { + c->flags |= CLIENT_REDRAWWINDOW; + break; + } + if (wp->flags & PANE_REDRAWSCROLLBAR) + c->flags |= CLIENT_REDRAWSCROLLBARS; + } return; } @@ -2391,7 +2402,8 @@ server_client_check_redraw(struct client *c) log_debug("%s: redraw pane %%%u", __func__, wp->id); redraw_pane(c, wp); - } else if (wp->flags & PANE_REDRAWSCROLLBAR) { + } else if ((wp->flags & PANE_REDRAWSCROLLBAR) || + (c->flags & CLIENT_REDRAWSCROLLBARS)) { log_debug("%s: redraw scrollbar %%%u", __func__, wp->id); redraw_pane_scrollbar(c, wp); @@ -2421,7 +2433,8 @@ server_client_check_redraw(struct client *c) * All the redraw flags can now be cleared. Also record how many bytes * were written. */ - c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE); + c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS| + CLIENT_STATUSFORCE); c->redraw = EVBUFFER_LENGTH(tty->out); log_debug("%s: redraw added %zu bytes", c->name, c->redraw); } diff --git a/sort.c b/sort.c index 31f6e82f1..1b71db494 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ +/* $OpenBSD: sort.c,v 1.10 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen @@ -32,7 +32,7 @@ sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *, u_int i; void *tmp, **ll; - if (sort_crit->order == SORT_END) + if (len < 2 || sort_crit->order == SORT_END) return; if (sort_crit->order == SORT_ORDER) { diff --git a/tmux.1 b/tmux.1 index 84c71184f..f961e01ae 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 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: August 20 2026 $ +.Dd $Mdocdate: September 1 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2208,14 +2208,14 @@ Move to the end of the next word. .Xc Same as .Ic next\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic next\-space\-end (vi: E) .Xc Same as .Ic next\-word\-end -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic other\-end (vi: o) @@ -2289,7 +2289,7 @@ Move to the previous word. .Xc Same as .Ic previous\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic rectangle\-on .Xc @@ -2574,8 +2574,8 @@ Word separators can be customized with the session option. Next word moves to the start of the next word, next word end to the end of the next word and previous word to the start of the previous word. -The three next and previous space keys work similarly but use a space alone as -the word separator. +The three next and previous space keys work similarly but treat all +non-whitespace characters as part of a word. Setting .Em word\-separators to the empty string makes next/previous word equivalent to next/previous space. @@ -4754,6 +4754,15 @@ for backspace. Set the number of buffers; as new buffers are added to the top of the stack, old ones are removed from the bottom if necessary to maintain this maximum length. +.It Xo Ic clear\-on\-attach +.Op Ic on | off +.Xc +When enabled (the default), the client terminal switches to the alternate +screen and clears it when a client is attached. +When disabled, +.Nm +does not enter the alternate screen; instead the existing terminal content is +scrolled into the scrollback buffer so it remains accessible. .It Xo Ic command\-alias[] .Ar name=value .Xc @@ -5104,6 +5113,8 @@ Supports title setting. .It usstyle Allows underscore style and colour to be set. +.It utf8 +Supports UTF\-8 output. .El .It Ic terminal\-overrides[] Ar string Allow terminal descriptions read using @@ -5644,6 +5655,11 @@ If set to both, a bell and a message are produced. Sets the session's conception of what characters are considered word separators, for the purposes of the next and previous word commands in copy mode. +A space in +.Ar string +matches any character with the Unicode +.Em White_Space +property. .El .Pp Available window options are: @@ -7271,7 +7287,10 @@ The following variables are available, where appropriate: .It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode" .It Li "cursor_x" Ta "" Ta "Cursor X position in pane" .It Li "cursor_y" Ta "" Ta "Cursor Y position in pane" +.It Li "history_added" Ta "" Ta "Number of lines scrolled into pane history" .It Li "history_bytes" Ta "" Ta "Number of bytes in window history" +.It Li "history_collected" Ta "" Ta "Number of oldest pane history lines discarded at the history limit" +.It Li "history_generation" Ta "" Ta "Generation incremented when pane history is cleared or reflowed" .It Li "history_limit" Ta "" Ta "Maximum window history lines" .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" @@ -7370,6 +7389,7 @@ The following variables are available, where appropriate: .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" .It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" +.It Li "pane_output_generation" Ta "" Ta "Generation incremented when pane output is processed" .It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)" .It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" diff --git a/tmux.h b/tmux.h index d1af1e5ff..d9ff3c6c6 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1429 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -483,6 +483,7 @@ enum tty_code_code { TTYC_ICH1, TTYC_IL, TTYC_IL1, + TTYC_IND, TTYC_INDN, TTYC_INVIS, TTYC_KCBT, @@ -1356,9 +1357,10 @@ struct window_pane { char tty[TTY_NAME_MAX]; int status; struct timeval dead_time; - struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ + struct cmdq_item *wait_item; struct spawn_editor_state *editor; + uint64_t output_generation; time_t last_output_time; time_t last_prompt_time; time_t cmd_start_time; @@ -2279,7 +2281,7 @@ struct client { #define CLIENT_STARTSERVER 0x10000000 #define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 -/* 0x80000000ULL unused */ +#define CLIENT_REDRAWSCROLLBARS 0x80000000ULL #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL #define CLIENT_CONTROL_WAITEXIT 0x200000000ULL #define CLIENT_WINDOWSIZECHANGED 0x400000000ULL @@ -4076,6 +4078,7 @@ void session_update_history(struct session *); /* utf8.c */ enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *); enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *); +int utf8_has_whitespace(const struct utf8_data *); void utf8_update_width_cache(void); utf8_char utf8_build_one(u_char); enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *); diff --git a/tty-features.c b/tty-features.c index e999fb599..0b12745e2 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.43 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -368,6 +368,13 @@ static const struct tty_feature tty_feature_progressbar = { 0 }; +/* Terminal supports UTF-8. */ +static const struct tty_feature tty_feature_utf8 = { + "utf8", + NULL, + 0 +}; + /* Available terminal features. */ static const struct tty_feature *const tty_features[] = { &tty_feature_256, @@ -390,7 +397,8 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_strikethrough, &tty_feature_sync, &tty_feature_title, - &tty_feature_usstyle + &tty_feature_usstyle, + &tty_feature_utf8 }; /* Parse features for client. */ @@ -473,6 +481,9 @@ tty_feature_present(struct tty_term *term, const char *name) u_int i; char *copy; + if (strcmp(name, "utf8") == 0) + return ((term->tty->client->flags & CLIENT_UTF8) != 0); + for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcmp(tf->name, name) == 0) { @@ -486,7 +497,8 @@ tty_feature_present(struct tty_term *term, const char *name) * We don't just have the feature flag set. Check if the capabilities * supported by the client are actual set instead. */ - if (tf == NULL || strcmp(name, "ignorefkeys") == 0) + if (tf == NULL || tf->capabilities == NULL || + strcmp(name, "ignorefkeys") == 0) return (0); if (tf->flags != 0 && (term->flags & tf->flags) != tf->flags) return (0); @@ -534,6 +546,8 @@ tty_apply_features(struct tty_term *term) } } term->flags |= tf->flags; + if (tf == &tty_feature_utf8) + c->flags |= CLIENT_UTF8; } if ((term->applied_features|feat) == term->applied_features) return (0); diff --git a/tty-term.c b/tty-term.c index 8e0248e84..6b149fcee 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -111,6 +111,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_ICH] = { TTYCODE_STRING, "ich" }, [TTYC_IL1] = { TTYCODE_STRING, "il1" }, [TTYC_IL] = { TTYCODE_STRING, "il" }, + [TTYC_IND] = { TTYCODE_STRING, "ind" }, [TTYC_INDN] = { TTYCODE_STRING, "indn" }, [TTYC_INVIS] = { TTYCODE_STRING, "invis" }, [TTYC_KCBT] = { TTYCODE_STRING, "kcbt" }, diff --git a/tty.c b/tty.c index e35ecb555..681d8291e 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.480 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -337,6 +337,7 @@ tty_start_tty(struct tty *tty) { struct client *c = tty->client; struct termios tio; + u_int i; setblocking(c->fd, 0); event_add(&tty->event_in, NULL); @@ -352,10 +353,21 @@ tty_start_tty(struct tty *tty) if (tcsetattr(c->fd, TCSANOW, &tio) == 0) tcflush(c->fd, TCOFLUSH); - tty_putcode(tty, TTYC_SMCUP); - + if (options_get_number(global_options, "clear-on-attach")) { + tty_putcode(tty, TTYC_SMCUP); + tty_putcode(tty, TTYC_CLEAR); + } else { + tty_putcode_ii(tty, TTYC_CSR, 0, tty->sy - 1); + tty_putcode_ii(tty, TTYC_CUP, 0, tty->sy - 1); + if (tty_term_has(tty->term, TTYC_INDN)) + tty_putcode_i(tty, TTYC_INDN, tty->sy + 1); + else if (tty_term_has(tty->term, TTYC_IND)) { + for (i = 0; i < tty->sy + 1; i++) + tty_putcode(tty, TTYC_IND); + } else + tty_putcode(tty, TTYC_CLEAR); + } tty_putcode(tty, TTYC_SMKX); - tty_putcode(tty, TTYC_CLEAR); if (tty_acs_needed(tty)) { log_debug("%s: using capabilities for ACS", c->name); @@ -471,7 +483,8 @@ tty_stop_tty(struct tty *tty) tty_raw(tty, tty_term_string(tty->term, TTYC_RMACS)); tty_raw(tty, tty_term_string(tty->term, TTYC_SGR0)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMKX)); - tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->cstyle != SCREEN_CURSOR_DEFAULT) { if (tty_term_has(tty->term, TTYC_SE)) tty_raw(tty, tty_term_string(tty->term, TTYC_SE)); @@ -496,7 +509,10 @@ tty_stop_tty(struct tty *tty) if (tty_use_margin(tty)) tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); - tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + else + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->term->flags & TERM_VT100LIKE) tty_raw(tty, "\033[?2031l"); diff --git a/utf8.c b/utf8.c index 887d2b0f9..68c37d362 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.72 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -603,6 +603,72 @@ utf8_towc(const struct utf8_data *ud, wchar_t *wc) return (UTF8_DONE); } +/* Check for a Unicode whitespace character. */ +int +utf8_has_whitespace(const struct utf8_data *ud) +{ + struct utf8_data tmp; + wchar_t wc; + u_int offset = 0, size; + u_char ch; + + while (offset < ud->size) { + ch = ud->data[offset]; + if (ch < 0x80) { + wc = ch; + size = 1; + } else { + if (ch >= 0xc2 && ch <= 0xdf) + size = 2; + else if (ch >= 0xe0 && ch <= 0xef) + size = 3; + else if (ch >= 0xf0 && ch <= 0xf4) + size = 4; + else + return (0); + if (size > ud->size - offset) + return (0); + + memset(&tmp, 0, sizeof tmp); + memcpy(tmp.data, ud->data + offset, size); + tmp.size = tmp.have = size; + if (utf8_towc(&tmp, &wc) != UTF8_DONE) + return (0); + } + offset += size; + + switch (wc) { + case 0x0009: + case 0x000A: + case 0x000B: + case 0x000C: + case 0x000D: + case 0x0020: + case 0x0085: + case 0x00A0: + case 0x1680: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: + case 0x2008: + case 0x2009: + case 0x200A: + case 0x2028: + case 0x2029: + case 0x202F: + case 0x205F: + case 0x3000: + return (1); + } + } + return (0); +} + /* Convert wide character to UTF-8 character. */ enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *ud) diff --git a/window-buffer.c b/window-buffer.c index ab15d87ed..bc25de66e 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -567,9 +567,7 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg) } oldbuf = paste_buffer_data(pb, &oldlen); - if (oldlen != '\0' && - oldbuf[oldlen - 1] != '\n' && - buf[len - 1] == '\n') + if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n') len--; if (len != 0) paste_replace(pb, buf, len); diff --git a/window-client.c b/window-client.c index f42aa5b7a..3c2f81097 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.49 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -102,7 +102,8 @@ static const char *window_client_info_lines[] = { WINDOW_CLIENT_FEATURE(sync) " " WINDOW_CLIENT_FEATURE(title), " #[#{E:tree-mode-border-style},acs]x#[default] " - WINDOW_CLIENT_FEATURE(usstyle), + WINDOW_CLIENT_FEATURE(usstyle) " " + WINDOW_CLIENT_FEATURE(utf8), "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", "#[fg=themelightgrey]prefix #[#{E:tree-mode-border-style},acs]x#[default] " diff --git a/window-copy.c b/window-copy.c index 9ae419fe7..48c2f7851 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2207,15 +2207,76 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct options *so = cs->s->options; struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; const char *s = args_string(cs->wargs, 0); + u_int sx, sy, ex, ey, fx, fy, x, y; if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) data->selflag = SEL_CHAR; else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { data->separators = options_get_string(so, "word-separators"); data->selflag = SEL_WORD; - } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) + } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) { data->selflag = SEL_LINE; + if (data->screen.sel == NULL) + return (WINDOW_COPY_CMD_MOVE); + + /* + * Line selection normally starts with select-line, which sets + * up the reset positions used when the cursor changes + * direction. Do the same when changing an existing selection + * to line mode. + */ + if (data->cursordrag == CURSORDRAG_SEL) { + fx = data->endselx; + fy = data->endsely; + } else { + fx = data->selx; + fy = data->sely; + } + + sx = data->selx; + sy = data->sely; + ex = data->endselx; + ey = data->endsely; + if (ey < sy || (ey == sy && ex < sx)) { + x = sx; sx = ex; ex = x; + y = sy; sy = ey; ey = y; + } + grid_reader_start(&gr, data->backing->grid, sx, sy); + grid_reader_cursor_start_of_line(&gr, 1); + grid_reader_get_cursor(&gr, &sx, &sy); + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_end_of_line(&gr, 1, 0); + grid_reader_get_cursor(&gr, &ex, &ey); + + data->rectflag = 0; + data->selrx = data->selx = sx; + data->selry = data->sely = sy; + data->endselrx = data->endselx = ex; + data->endselry = data->endsely = ey; + + x = data->cx; + y = screen_hsize(data->backing) + data->cy - data->oy; + data->dx = fx; + data->dy = fy; + if (data->cursordrag != CURSORDRAG_NONE && + (y < fy || (y == fy && x < fx))) { + data->lineflag = LINE_SEL_RIGHT_LEFT; + data->cursordrag = CURSORDRAG_SEL; + window_copy_scroll_to(wme, sx, sy, 1); + } else { + data->lineflag = LINE_SEL_LEFT_RIGHT; + if (data->cursordrag != CURSORDRAG_NONE) { + data->cursordrag = CURSORDRAG_ENDSEL; + x = window_copy_cursor_limit(wme, ey, 0); + window_copy_scroll_to(wme, x, ey, 1); + } + } + if (data->cursordrag == CURSORDRAG_NONE) + window_copy_set_selection(wme, 0, 0); + return (WINDOW_COPY_CMD_REDRAW); + } return (WINDOW_COPY_CMD_MOVE); } @@ -4301,7 +4362,7 @@ window_copy_stringify(struct grid *gd, u_int py, u_int first, u_int last, } if (dlen == 1) buf[bx++] = *d; - else { + else if (dlen != 0) { memcpy(buf + bx, d, dlen); bx += dlen; } @@ -4851,6 +4912,8 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp, cflags |= REG_ICASE; if (regcomp(®, sbuf, cflags) != 0) { free(sbuf); + free(data->searchmark); + data->searchmark = NULL; return (0); } free(sbuf); diff --git a/window-panes.c b/window-panes.c index f1104b4f3..e65e59606 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.5 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1009,6 +1009,7 @@ window_panes_run_command(struct window_panes_modedata *data, struct client *c, } else { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } free(expanded); } diff --git a/window-switch.c b/window-switch.c index 6712d8a89..f9cb16ec5 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-switch.c,v 1.2 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-switch.c,v 1.3 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -227,7 +227,8 @@ window_switch_build(struct window_switch_modedata *data) m = xreallocarray(m, n + 1, sizeof *m); m[n++] = item; } - qsort(m, n, sizeof *m, window_switch_compare); + if (n > 1) + qsort(m, n, sizeof *m, window_switch_compare); free(data->matches); data->matches = m; diff --git a/window-tree.c b/window-tree.c index 342a1c3bc..7e90a5a2f 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.96 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -642,6 +642,7 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -790,6 +791,7 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -1020,6 +1022,10 @@ window_tree_swap(void *cur_itemdata, void *other_itemdata, window_tree_pull_item(other, &other_session, &other_winlink, &other_pane); + if (cur_session == NULL || cur_winlink == NULL) + return (0); + if (other_session == NULL || other_winlink == NULL) + return (0); if (cur_session != other_session) return (0); diff --git a/window.c b/window.c index e6425e7c4..6b6c13075 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.370 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1477,6 +1477,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp) void window_pane_scrollbar_redraw(struct window_pane *wp) { + if (!window_pane_scrollbar_visible(wp)) + return; if (window_pane_scrollbar_overlay_visible(wp)) { wp->flags |= PANE_REDRAW; return; @@ -1544,16 +1546,16 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); - screen_free(&wp->status_screen); screen_free(&wp->base); - + free(wp->r.ranges); options_free(wp->options); free((void *)wp->cwd); free(wp->shell); cmd_free_argv(wp->argc, wp->argv); colour_palette_free(&wp->palette); style_ranges_free(&wp->border_status_line.ranges); + free(wp->border_status_line.expanded); free(wp); }