From 5047e4eb1a2d0ecdde2148a8b7a82c88d3fd500b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 19 Aug 2026 09:34:39 +0100 Subject: [PATCH] Add a way to make a modal pane capture all key presses and use for display-popup compatibility. --- cmd-display-menu.c | 1 + cmd-split-window.c | 6 ++-- regress/modal-pane.sh | 74 +++++++++++++++++++++++++++++++++++++++++++ server-client.c | 53 ++++++++++++++++++++++++------- tmux.1 | 5 ++- tmux.h | 1 + 6 files changed, 126 insertions(+), 14 deletions(-) diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 3eed593c9..c7da24109 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -502,6 +502,7 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) window_pop_modal_zoom(w); goto fail; } + new_wp->flags |= PANE_CAPTUREALLKEYS; options_set_number(new_wp->options, "pane-border-lines", lines); if (args_has(args, 'E') > 1) diff --git a/cmd-split-window.c b/cmd-split-window.c index 8beeac557..d30aedaca 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bCdefhIklMOPvWZ] [-B border-lines] " + .args = { "bB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-bCdefhIkKLMOPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -202,6 +202,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) */ goto fail; } + if (args_has(args, 'K') && args_has(args, 'O')) + new_wp->flags |= PANE_CAPTUREALLKEYS; if (args_has(args, 'C') && args_has(args, 'O')) new_wp->flags |= PANE_CLOSEONCLICK; diff --git a/regress/modal-pane.sh b/regress/modal-pane.sh index cd891281d..8346cc4fa 100644 --- a/regress/modal-pane.sh +++ b/regress/modal-pane.sh @@ -105,6 +105,24 @@ drag() sleep 1 } +meta_drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + cleanup check_ok new-session -d -s modal -x 80 -y 24 'cat' @@ -304,5 +322,61 @@ sleep 1 must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' must_equal "$(fmt modal:0 '#{pane_id}')" "$p0" +ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') || + fail "new-pane -K without -O failed" +check_ok kill-pane -t "$ignored" + +$TMUX set -g @modal-prefix no +$TMUX set -g @modal-root no +$TMUX bind -n z set -g @modal-root yes + +modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "new-pane -OK failed" +sleep 1 +$TMUX2 send-keys -t "$OUTER" C-b x z Enter +sleep 1 +must_equal "$($TMUX show -gv @modal-prefix)" no +must_equal "$($TMUX show -gv @modal-root)" no +case "$($TMUX capture-pane -pt "$modal")" in +*xz*) ;; +*) fail "keys did not reach key-capturing modal pane" ;; +esac +left=$(fmt "$modal" '#{pane_left}') +top=$(fmt "$modal" '#{pane_top}') +meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4)) +new_left=$(fmt "$modal" '#{pane_left}') +new_top=$(fmt "$modal" '#{pane_top}') +[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] || + fail "key-capturing modal pane did not move" +check_ok kill-pane -t "$modal" +sleep 1 + +$TMUX bind P display-popup -E -t "$p0" -w 20 -h 5 'cat' +$TMUX2 send-keys -t "$OUTER" C-b P +sleep 1 +modal=$(fmt modal:0 '#{window_modal_pane}') +[ -n "$modal" ] || fail "display-popup did not create a modal pane" +$TMUX2 send-keys -t "$OUTER" C-b x z Enter +sleep 1 +must_equal "$($TMUX show -gv @modal-prefix)" no +must_equal "$($TMUX show -gv @modal-root)" no +case "$($TMUX capture-pane -pt "$modal")" in +*xz*) ;; +*) fail "keys did not reach display-popup pane" ;; +esac +check_ok kill-pane -t "$modal" +sleep 1 + +$TMUX bind D display-popup -t "$p0" -w 20 -h 5 'printf done' +$TMUX2 send-keys -t "$OUTER" C-b D +sleep 2 +modal=$(fmt modal:0 '#{window_modal_pane}') +[ -n "$modal" ] || fail "retained display-popup was not created" +must_equal "$(fmt "$modal" '#{pane_dead}')" 1 +$TMUX2 send-keys -t "$OUTER" a +sleep 1 +must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' + cleanup exit 0 diff --git a/server-client.c b/server-client.c index 771de0313..f57dc5d10 100644 --- a/server-client.c +++ b/server-client.c @@ -45,6 +45,7 @@ static void server_client_set_path(struct client *); static void server_client_set_progress_bar(struct client *); static void server_client_reset_state(struct client *); static void server_client_update_latest(struct client *); +static int server_client_handle_dead_key(struct window_pane *, key_code); static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); @@ -1270,6 +1271,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd) return (repeat); } +/* Handle a key press on a dead pane waiting for a key. */ +static int +server_client_handle_dead_key(struct window_pane *wp, key_code key) +{ + if (wp == NULL || + (~wp->flags & PANE_EXITED) || + KEYC_IS_MOUSE(key) || + KEYC_IS_PASTE(key) || + options_get_number(wp->options, "remain-on-exit") != 3) + return (0); + options_set_number(wp->options, "remain-on-exit", 0); + server_destroy_pane(wp, 0); + return (1); +} + /* * Handle data key input from client. This owns and can modify the key event it * is given and is responsible for freeing it. @@ -1355,6 +1371,14 @@ server_client_key_callback(struct cmdq_item *item, void *data) server_client_is_assume_paste(c)) goto paste_key; + /* Forward keys directly if this pane is capturing all keys. */ + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + (~wp->flags & PANE_EXITED) && + !KEYC_IS_MOUSE(key) && + TAILQ_EMPTY(&wp->modes)) + 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. @@ -1519,15 +1543,8 @@ try_again: } forward_key: - if (wp != NULL && - (wp->flags & PANE_EXITED) && - !KEYC_IS_MOUSE(key) && - !KEYC_IS_PASTE(key) && - options_get_number(wp->options, "remain-on-exit") == 3) { - options_set_number(wp->options, "remain-on-exit", 0); - server_destroy_pane(wp, 0); + if (server_client_handle_dead_key(wp, key)) goto out; - } if (c->flags & CLIENT_READONLY) goto out; if (wp != NULL) @@ -1611,9 +1628,9 @@ server_client_handle_key0(struct client *c, struct key_event *event, } /* - * Key presses in the command prompt are a special case. The queue might - * be blocked so they need to be processed immediately rather than - * queued. + * Key presses for panes capturing all keys and in the command prompt are + * a special case. The queue might be blocked so they need to be processed + * immediately rather than queued. */ if (~c->flags & CLIENT_READONLY) { if (c->message_string != NULL) { @@ -1622,6 +1639,20 @@ server_client_handle_key0(struct client *c, struct key_event *event, status_message_clear(c); } + wp = s->curw->window->active; + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + TAILQ_EMPTY(&wp->modes) && + !KEYC_IS_MOUSE(event->key)) { + if (server_client_handle_dead_key(wp, event->key)) + return (0); + if (~wp->flags & PANE_EXITED) { + window_pane_key(wp, c, s, s->curw, event->key, + &event->m); + return (0); + } + } + if (server_client_handle_menu_key(c, event)) return (0); if (c->prompt != NULL) { diff --git a/tmux.1 b/tmux.1 index 460948592..48d47c321 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3644,7 +3644,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bCdefhIkLMOPvWZ +.Op Fl bCdefhIkKLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3701,6 +3701,9 @@ A modal pane is always the active pane and prevents interaction with any other panes while it is active. A window can only have one modal pane and it must be a floating pane. With +.Fl K , +all keys, including the prefix key, are passed directly to the modal pane. +With .Fl C , the modal pane is closed when the mouse is clicked outside it. .Pp diff --git a/tmux.h b/tmux.h index 41ffa187a..630430fd8 100644 --- a/tmux.h +++ b/tmux.h @@ -1335,6 +1335,7 @@ struct window_pane { #define PANE_CMDRUNNING 0x20000 #define PANE_ACTIVITY 0x40000 #define PANE_CLOSEONCLICK 0x80000 +#define PANE_CAPTUREALLKEYS 0x100000 bitstr_t *sync_dirty; u_int sync_dirty_size;