diff --git a/Makefile.am b/Makefile.am index ba9142fec..b0244f0f5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -102,6 +102,7 @@ dist_tmux_SOURCES = \ cmd-display-panes.c \ cmd-find-window.c \ cmd-find.c \ + cmd-hide-pane.c \ cmd-if-shell.c \ cmd-join-pane.c \ cmd-kill-pane.c \ @@ -149,6 +150,7 @@ dist_tmux_SOURCES = \ cmd-source-file.c \ cmd-split-window.c \ cmd-swap-pane.c \ + cmd-tile-float-pane.c \ cmd-swap-window.c \ cmd-switch-client.c \ cmd-unbind-key.c \ diff --git a/cmd-find.c b/cmd-find.c index d2f902516..76f2742a2 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -994,15 +994,20 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, } else if (cmd_find_from_client(¤t, cmdq_get_client(item), flags) == 0) { fs->current = ¤t; + /* No active pane, window empty, return the window instead. */ + if (current.wp == NULL) { + type = CMD_FIND_WINDOW; + } log_debug("%s: current is from client", __func__); } else { if (~flags & CMD_FIND_QUIET) cmdq_error(item, "no current target"); goto error; } + /* if (!cmd_find_valid_state(fs->current)) fatalx("invalid current find state"); - + */ /* An empty or NULL target is the current. */ if (target == NULL || *target == '\0') goto current; @@ -1041,7 +1046,7 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, fs->w = fs->wl->window; fs->wp = fs->w->active; } - break; + goto found; } if (fs->wp == NULL) { if (~flags & CMD_FIND_QUIET) diff --git a/cmd-hide-pane.c b/cmd-hide-pane.c new file mode 100644 index 000000000..e9a9fe1d7 --- /dev/null +++ b/cmd-hide-pane.c @@ -0,0 +1,223 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* + * Hide or show panes. + */ + +static enum cmd_retval cmd_hide_pane_hide_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_hide_pane_show_exec(struct cmd *, struct cmdq_item *); + +static enum cmd_retval cmd_hide_pane_hide(struct window *, struct window_pane *, + struct cmdq_item *); +static enum cmd_retval cmd_hide_pane_show(struct window *, struct window_pane *); + +const struct cmd_entry cmd_hide_pane_entry = { + .name = "hide-pane", + .alias = "hidep", + + .args = { "at:", 0, 1, NULL }, + .usage = "[-a] " CMD_TARGET_PANE_USAGE, + + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = CMD_AFTERHOOK, + .exec = cmd_hide_pane_hide_exec +}; + +const struct cmd_entry cmd_show_pane_entry = { + .name = "show-pane", + .alias = "showp", + + .args = { "at:", 0, 1, NULL }, + .usage = "[-a] " CMD_TARGET_PANE_USAGE, + + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = CMD_AFTERHOOK, + .exec = cmd_hide_pane_show_exec +}; + + +static enum cmd_retval +cmd_hide_pane_hide_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct winlink *wl = target->wl; + struct window *w = wl->window; + struct window_pane *wp, *active_pane = w->active; + u_int id; + char *cause = NULL; + enum cmd_retval rv; + + if (args_has(args, 'a')) { + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_visible(wp) || wp == active_pane) + continue; + rv = cmd_hide_pane_hide(w, wp, item); + if (rv != CMD_RETURN_NORMAL) + return (rv); + } + return (CMD_RETURN_NORMAL); + } else { + wp = target->wp; + if (wp == NULL) { + id = args_strtonum_and_expand(args, 't', 0, INT_MAX, + item, &cause); + if (cause != NULL) { + cmdq_error(item, "%s target pane", cause); + return (CMD_RETURN_ERROR); + } + wp = window_pane_find_by_id(id); + } + if (wp == NULL) { + cmdq_error(item, "No target pane to hide."); + return (CMD_RETURN_ERROR); + } + return (cmd_hide_pane_hide(w, wp, item)); + } +} + +static enum cmd_retval +cmd_hide_pane_show_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct winlink *wl = target->wl; + struct window *w = wl->window; + struct window_pane *wp; + u_int id; + char *cause = NULL; + enum cmd_retval rv; + + if (args_has(args, 'a')) { + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_visible(wp)) + continue; + rv = cmd_hide_pane_show(w, wp); + if (rv != CMD_RETURN_NORMAL) + return (rv); + } + return (CMD_RETURN_NORMAL); + } else { + wp = target->wp; + if (wp == NULL) { + id = args_strtonum_and_expand(args, 't', 0, INT_MAX, + item, &cause); + if (cause != NULL) { + cmdq_error(item, "%s target pane", cause); + return (CMD_RETURN_ERROR); + } + wp = window_pane_find_by_id(id); + } + if (wp == NULL) { + cmdq_error(item, "No target pane to show."); + return (CMD_RETURN_ERROR); + } + return (cmd_hide_pane_show(w, wp)); + } +} + +static enum cmd_retval +cmd_hide_pane_hide(struct window *w, struct window_pane *wp, + __unused struct cmdq_item *item) +{ + struct window_pane *pwp = NULL; + + if (wp->flags & PANE_HIDDEN) + return (CMD_RETURN_NORMAL); + + if (wp == w->active) { + /* + * Unzoom before searching: under zoom, window_pane_visible + * returns false for every non-active pane. + */ + if (w->flags & WINDOW_ZOOMED) + window_unzoom(w, 1); + /* Find previous active pane. */ + TAILQ_FOREACH(pwp, &w->last_panes, sentry) { + if (pwp != wp && window_pane_visible(pwp)) + break; + } + if (pwp == NULL) { + TAILQ_FOREACH(pwp, &w->z_index, zentry) { + if (pwp != wp && + window_pane_visible(pwp)) + break; + } + } + } + + wp->flags |= PANE_HIDDEN; + + if (w->layout_root != NULL) { + wp->saved_layout_cell = wp->layout_cell; + layout_hide_cell(w, wp->layout_cell); + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + } + + window_pane_stack_remove(&w->last_panes, wp); + if (pwp != NULL) { + window_set_active_pane(w, pwp, 1); + } else if (wp == w->active) { + /* No visible previous active pane; null active pane + * to show dots background. */ + w->active = NULL; + if (options_get_number(global_options, "focus-events")) + window_pane_update_focus(wp); + notify_window("window-pane-changed", w); + notify_window("window-layout-changed", w); + server_redraw_window(w); + } else { + notify_window("window-layout-changed", w); + server_redraw_window(w); + } + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_hide_pane_show(struct window *w, struct window_pane *wp) +{ + wp->flags &= ~PANE_HIDDEN; + + /* Fix pane offsets and sizes. */ + if (w->layout_root != NULL && wp->saved_layout_cell != NULL) { + wp->layout_cell = wp->saved_layout_cell; + wp->saved_layout_cell = NULL; + layout_show_cell(w, wp->layout_cell); + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + } + + window_set_active_pane(w, wp, 1); + + notify_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index 48785c921..8a83a67af 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -79,12 +79,6 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) if (src_wp == dst_wp) goto out; - if (window_pane_is_floating(src_wp) || - window_pane_is_floating(dst_wp)) { - cmdq_error(item, "cannot swap floating panes"); - return (CMD_RETURN_ERROR); - } - server_client_remove_pane(src_wp); server_client_remove_pane(dst_wp); @@ -114,10 +108,6 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) dst_wp->layout_cell = src_lc; dst_lc->wp = src_wp; src_wp->layout_cell = dst_lc; - if (window_pane_is_floating(src_wp) != window_pane_is_floating(dst_wp)) { - src_wp->layout_cell->flags ^= LAYOUT_CELL_FLOATING; - dst_wp->layout_cell->flags ^= LAYOUT_CELL_FLOATING; - } src_wp->window = dst_w; options_set_parent(src_wp->options, dst_w->options); diff --git a/cmd-tile-float-pane.c b/cmd-tile-float-pane.c new file mode 100644 index 000000000..19dfa4fc9 --- /dev/null +++ b/cmd-tile-float-pane.c @@ -0,0 +1,363 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* + * float-pane: lift a tiled pane out of the layout tree into a floating pane. + * tile-pane: insert a floating pane back into the tiled layout. + * + * saved_layout_cell is reused to remember the pane's tiled slot while it is + * floating, using the same mechanism as hide-pane. The cell's wp pointer + * is cleared while the pane is floating so that layout helpers treat the slot + * as empty. + */ + +static enum cmd_retval cmd_float_pane_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_tile_pane_exec(struct cmd *, struct cmdq_item *); + +const struct cmd_entry cmd_float_pane_entry = { + .name = "float-pane", + .alias = NULL, + + .args = { "t:x:y:w:h:", 0, 0, NULL }, + .usage = "[-h height] [-w width] [-x x] [-y y] " + CMD_TARGET_PANE_USAGE, + + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = CMD_AFTERHOOK, + .exec = cmd_float_pane_exec +}; + +const struct cmd_entry cmd_tile_pane_entry = { + .name = "tile-pane", + .alias = NULL, + + .args = { "t:", 0, 0, NULL }, + .usage = CMD_TARGET_PANE_USAGE, + + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = CMD_AFTERHOOK, + .exec = cmd_tile_pane_exec +}; + +/* + * Parse geometry arguments for float-pane. + * Returns 0 on success, -1 on error (error message already set on item). + * x/y/sx/sy are set to parsed values or cascade defaults. + * last_x/last_y are the static cascade counters; pass the address of the + * caller's statics. + */ +static int +cmd_float_pane_parse_geometry(struct args *args, struct cmdq_item *item, + struct window *w, int *out_x, int *out_y, u_int *out_sx, u_int *out_sy, + int *last_x, int *last_y) +{ + char *cause = NULL; + int x, y; + u_int sx, sy; + + /* Default size: half the window. */ + sx = w->sx / 2; + sy = w->sy / 2; + + if (args_has(args, 'w')) { + sx = args_strtonum_and_expand(args, 'w', 1, USHRT_MAX, item, + &cause); + if (cause != NULL) { + cmdq_error(item, "width %s", cause); + free(cause); + return (-1); + } + } + if (args_has(args, 'h')) { + sy = args_strtonum_and_expand(args, 'h', 1, USHRT_MAX, item, + &cause); + if (cause != NULL) { + cmdq_error(item, "height %s", cause); + free(cause); + return (-1); + } + } + + /* Default position: cascade from (5,5), step +5, wrap at window edge. */ + if (args_has(args, 'x')) { + x = args_strtonum_and_expand(args, 'x', SHRT_MIN, SHRT_MAX, + item, &cause); + if (cause != NULL) { + cmdq_error(item, "x %s", cause); + free(cause); + return (-1); + } + } else { + if (*last_x == 0) { + x = 5; + } else { + x = (*last_x += 5); + if (*last_x > (int)w->sx) + x = *last_x = 5; + } + } + if (args_has(args, 'y')) { + y = args_strtonum_and_expand(args, 'y', SHRT_MIN, SHRT_MAX, + item, &cause); + if (cause != NULL) { + cmdq_error(item, "y %s", cause); + free(cause); + return (-1); + } + } else { + if (*last_y == 0) { + y = 5; + } else { + y = (*last_y += 5); + if (*last_y > (int)w->sy) + y = *last_y = 5; + } + } + + *last_x = x; + *last_y = y; + *out_x = x; + *out_y = y; + *out_sx = sx; + *out_sy = sy; + return (0); +} + +static enum cmd_retval +cmd_float_pane_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct window *w = target->wl->window; + struct window_pane *wp = target->wp; + static int last_x = 0, last_y = 0; + int x, y; + u_int sx, sy; + struct layout_cell *lc; + + if (window_pane_is_floating(wp)) { + cmdq_error(item, "pane is already floating"); + return (CMD_RETURN_ERROR); + } + if (wp->flags & PANE_HIDDEN) { + cmdq_error(item, "can't float a hidden pane"); + return (CMD_RETURN_ERROR); + } + if (w->flags & WINDOW_ZOOMED) { + cmdq_error(item, "can't float a pane while window is zoomed"); + return (CMD_RETURN_ERROR); + } + + /* + * If no geometry was given explicitly and we have a saved floating + * position from a previous tile-pane, restore it. + */ + if ((wp->flags & PANE_SAVED_FLOAT) && + !args_has(args, 'x') && !args_has(args, 'y') && + !args_has(args, 'w') && !args_has(args, 'h')) { + x = wp->saved_float_xoff; + y = wp->saved_float_yoff; + sx = wp->saved_float_sx; + sy = wp->saved_float_sy; + } else { + if (cmd_float_pane_parse_geometry(args, item, w, &x, &y, &sx, + &sy, &last_x, &last_y) != 0) + return (CMD_RETURN_ERROR); + } + + /* + * Remove the pane from the tiled layout tree so neighbours reclaim + * the space. layout_close_pane calls layout_destroy_cell which frees + * the tiled layout_cell and sets wp->layout_cell = NULL via + * layout_free_cell. It also calls layout_fix_offsets/fix_panes and + * notify_window, which is fine to do here before we set up the + * floating cell. + */ + layout_close_pane(wp); /* wp->layout_cell is NULL afterwards */ + + /* Create a detached floating cell with the requested geometry. */ + lc = layout_create_cell(NULL); + lc->xoff = x; + lc->yoff = y; + lc->sx = sx; + lc->sy = sy; + layout_make_leaf(lc, wp); /* sets wp->layout_cell = lc, lc->wp = wp */ + + lc->flags |= LAYOUT_CELL_FLOATING; + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_HEAD(&w->z_index, wp, zentry); + + if (w->layout_root != NULL) + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + notify_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_tile_pane_exec(struct cmd *self, struct cmdq_item *item) +{ + __attribute((unused)) struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct window *w = target->wl->window; + struct window_pane *wp = target->wp; + struct window_pane *target_wp, *wpiter; + struct layout_cell *float_lc, *lc; + int was_hidden; + + if (!window_pane_is_floating(wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } + if (w->flags & WINDOW_ZOOMED) { + cmdq_error(item, "can't tile a pane while window is zoomed"); + return (CMD_RETURN_ERROR); + } + + was_hidden = (wp->flags & PANE_HIDDEN) != 0; + + /* + * Save the floating geometry so we can restore it next time this pane + * is floated without an explicit position/size. + */ + float_lc = wp->layout_cell; + wp->saved_float_xoff = float_lc->xoff; + wp->saved_float_yoff = float_lc->yoff; + wp->saved_float_sx = float_lc->sx; + wp->saved_float_sy = float_lc->sy; + wp->flags |= PANE_SAVED_FLOAT; + + /* + * If the pane is also hidden, clear saved_layout_cell before + * freeing the floating cell — otherwise the pointer would dangle. + */ + if (was_hidden) + wp->saved_layout_cell = NULL; + + /* + * Free the detached floating cell. Clear its wp pointer first so + * layout_free_cell's WINDOWPANE case does not corrupt wp->layout_cell. + */ + float_lc->wp = NULL; + layout_free_cell(float_lc); + wp->layout_cell = NULL; + + /* + * Find the best tiled pane to split after, prefer a visible (non- + * hidden) tiled pane. If all tiled panes are hidden, fall back + * to any tiled pane so the new pane enters the existing tree rather + * than becoming a disconnected root. + */ + target_wp = NULL; + if (w->active != NULL && !window_pane_is_floating(w->active) && + !(w->active->flags & PANE_HIDDEN)) + target_wp = w->active; + if (target_wp == NULL) { + TAILQ_FOREACH(wpiter, &w->last_panes, sentry) { + if (!(wpiter->flags & PANE_HIDDEN) && + !window_pane_is_floating(wpiter) && + window_pane_visible(wpiter)) { + target_wp = wpiter; + break; + } + } + } + if (target_wp == NULL) { + TAILQ_FOREACH(wpiter, &w->panes, entry) { + if (!(wpiter->flags & PANE_HIDDEN) && + !window_pane_is_floating(wpiter) && + window_pane_visible(wpiter)) { + target_wp = wpiter; + break; + } + } + } + /* Fall back to any tiled pane (even hidden) to stay in the tree. */ + if (target_wp == NULL) { + TAILQ_FOREACH(wpiter, &w->panes, entry) { + if (!window_pane_is_floating(wpiter)) { + target_wp = wpiter; + break; + } + } + } + if (target_wp != NULL) { + lc = layout_split_pane(target_wp, LAYOUT_TOPBOTTOM, -1, 0); + if (lc == NULL) + lc = layout_split_pane(target_wp, LAYOUT_LEFTRIGHT, + -1, 0); + if (lc == NULL) { + cmdq_error(item, "not enough space to tile pane"); + return (CMD_RETURN_ERROR); + } + layout_assign_pane(lc, wp, 0); + /* + * Redistribute space equally among all visible panes at this + * level, so the new pane gets an equal share rather than just + * half of the split target. + */ + if (wp->layout_cell != NULL && wp->layout_cell->parent != NULL) + layout_redistribute_cells(w, wp->layout_cell->parent, + wp->layout_cell->parent->type); + } else { + /* + * No tiled panes at all: make this pane the sole tiled pane + * (new layout root). + */ + lc = layout_create_cell(NULL); + lc->sx = w->sx; + lc->sy = w->sy; + lc->xoff = 0; + lc->yoff = 0; + w->layout_root = lc; + layout_make_leaf(lc, wp); + } + + /* + * If the pane was hidden while floating, record its new tiled cell + * as the saved cell so 'show' can restore it correctly. + */ + if (was_hidden) + wp->saved_layout_cell = wp->layout_cell; + + lc->flags &= ~LAYOUT_CELL_FLOATING; + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + + if (!(wp->flags & PANE_HIDDEN)) + window_set_active_pane(w, wp, 1); + + if (w->layout_root != NULL) + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + notify_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} diff --git a/cmd.c b/cmd.c index 6916da706..3c812c8f8 100644 --- a/cmd.c +++ b/cmd.c @@ -69,6 +69,8 @@ extern const struct cmd_entry cmd_load_buffer_entry; extern const struct cmd_entry cmd_lock_client_entry; extern const struct cmd_entry cmd_lock_server_entry; extern const struct cmd_entry cmd_lock_session_entry; +extern const struct cmd_entry cmd_float_pane_entry; +extern const struct cmd_entry cmd_hide_pane_entry; extern const struct cmd_entry cmd_move_pane_entry; extern const struct cmd_entry cmd_move_window_entry; extern const struct cmd_entry cmd_new_pane_entry; @@ -106,6 +108,7 @@ extern const struct cmd_entry cmd_show_environment_entry; extern const struct cmd_entry cmd_show_hooks_entry; extern const struct cmd_entry cmd_show_messages_entry; extern const struct cmd_entry cmd_show_options_entry; +extern const struct cmd_entry cmd_show_pane_entry; extern const struct cmd_entry cmd_show_prompt_history_entry; extern const struct cmd_entry cmd_show_window_options_entry; extern const struct cmd_entry cmd_source_file_entry; @@ -117,6 +120,7 @@ extern const struct cmd_entry cmd_swap_window_entry; extern const struct cmd_entry cmd_switch_client_entry; extern const struct cmd_entry cmd_unbind_key_entry; extern const struct cmd_entry cmd_unlink_window_entry; +extern const struct cmd_entry cmd_tile_pane_entry; extern const struct cmd_entry cmd_wait_for_entry; const struct cmd_entry *cmd_table[] = { @@ -162,6 +166,8 @@ const struct cmd_entry *cmd_table[] = { &cmd_lock_client_entry, &cmd_lock_server_entry, &cmd_lock_session_entry, + &cmd_float_pane_entry, + &cmd_hide_pane_entry, &cmd_move_pane_entry, &cmd_move_window_entry, &cmd_new_pane_entry, @@ -199,6 +205,7 @@ const struct cmd_entry *cmd_table[] = { &cmd_show_hooks_entry, &cmd_show_messages_entry, &cmd_show_options_entry, + &cmd_show_pane_entry, &cmd_show_prompt_history_entry, &cmd_show_window_options_entry, &cmd_source_file_entry, @@ -210,6 +217,7 @@ const struct cmd_entry *cmd_table[] = { &cmd_switch_client_entry, &cmd_unbind_key_entry, &cmd_unlink_window_entry, + &cmd_tile_pane_entry, &cmd_wait_for_entry, NULL }; diff --git a/format.c b/format.c index 129ad582d..99f9287dd 100644 --- a/format.c +++ b/format.c @@ -2145,6 +2145,20 @@ format_cb_pane_height(struct format_tree *ft) return (NULL); } +/* Callback for pane_hidden_flag. */ +static void * +format_cb_pane_hidden_flag(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) { + if (wp->flags & PANE_HIDDEN) + return (xstrdup("1")); + return (xstrdup("0")); + } + return (NULL); +} + /* Callback for pane_id. */ static void * format_cb_pane_id(struct format_tree *ft) @@ -3450,6 +3464,9 @@ static const struct format_table_entry format_table[] = { { "pane_height", FORMAT_TABLE_STRING, format_cb_pane_height }, + { "pane_hidden_flag", FORMAT_TABLE_STRING, + format_cb_pane_hidden_flag + }, { "pane_id", FORMAT_TABLE_STRING, format_cb_pane_id }, diff --git a/key-bindings.c b/key-bindings.c index eee29ae02..647971a2b 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -65,7 +65,7 @@ " ''" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' 'u' {swap-pane -U}" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' 'd' {swap-pane -D}" \ - " '#{?#{!:#{pane_floating_flag}},#{?pane_marked_set,,-}Swap Marked,}' 's' {swap-pane}" \ + " '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane}" \ " ''" \ " 'Kill' 'X' {kill-pane}" \ " 'Respawn' 'R' {respawn-pane -k}" \ @@ -351,6 +351,8 @@ key_bindings_init(void) { static const char *const defaults[] = { /* Prefix keys. */ + "bind -N 'Hide pane' _ { hide-pane }", + "bind -N 'Send the prefix key' C-b { send-prefix }", "bind -N 'Rotate through the panes' C-o { rotate-window }", "bind -N 'Suspend the current client' C-z { suspend-client }", @@ -363,6 +365,7 @@ key_bindings_init(void) "bind -N 'Kill current window' & { confirm-before -p\"kill-window #W? (y/n)\" kill-window }", "bind -N 'Prompt for window index to select' \"'\" { command-prompt -T window-target -pindex { select-window -t ':%%' } }", "bind -N 'New floating pane' * { new-pane }", + "bind -N 'Toggle pane between floating and tiled' @ { if -F '#{pane_floating_flag}' { tile-pane } { float-pane } }", "bind -N 'Switch to previous client' ( { switch-client -p }", "bind -N 'Switch to next client' ) { switch-client -n }", "bind -N 'Rename current window' , { command-prompt -I'#W' { rename-window -- '%%' } }", @@ -468,7 +471,7 @@ key_bindings_init(void) "bind -n MouseDrag1Border { resize-pane -M }", /* Mouse button 1 down on status line. */ - "bind -n MouseDown1Status { switch-client -t= }", + "bind -n MouseDown1Status { if -F '#{&&:#{pane_active},#{!#{pane_hidden_flag}}}' { hide-pane -t= } { switch-client -t= } }", "bind -n C-MouseDown1Status { swap-window -t@ }", /* Mouse button 1 down on default pane-border-format */ diff --git a/layout.c b/layout.c index 0beb57920..ad11d0133 100644 --- a/layout.c +++ b/layout.c @@ -250,7 +250,7 @@ layout_fix_offsets1(struct layout_cell *lc) if (lc->type == LAYOUT_LEFTRIGHT) { xoff = lc->xoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; lcchild->xoff = xoff; lcchild->yoff = lc->yoff; @@ -261,7 +261,7 @@ layout_fix_offsets1(struct layout_cell *lc) } else { yoff = lc->yoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; lcchild->xoff = lc->xoff; lcchild->yoff = yoff; @@ -288,6 +288,33 @@ layout_fix_offsets(struct window *w) layout_fix_offsets1(lc); } +/* + * Not all cells are drawn within the tiled grid of a layout. This predicate + * isolates that logic. + */ +int +layout_cell_is_tiled(struct layout_cell *lc) +{ + return ((~lc->flags & LAYOUT_CELL_HIDDEN) && + (~lc->flags & LAYOUT_CELL_FLOATING)); +} + +static int +layout_cell_is_first_tiled(struct layout_cell *lc) +{ + struct layout_cell *lcchild, *lcparent = lc->parent; + + if (lcparent == NULL) + return (layout_cell_is_tiled(lc)); + + TAILQ_FOREACH(lcchild, &lcparent->cells, entry) { + if (layout_cell_is_tiled(lcchild)) + break; + } + + return (lcchild == lc); +} + /* Is this a top cell? */ static int layout_cell_is_top(struct window *w, struct layout_cell *lc) @@ -299,7 +326,7 @@ layout_cell_is_top(struct window *w, struct layout_cell *lc) if (next == NULL) return (0); if (next->type == LAYOUT_TOPBOTTOM && - lc != TAILQ_FIRST(&next->cells)) + !layout_cell_is_first_tiled(lc)) return (0); lc = next; } @@ -491,7 +518,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, /* Child cell runs in a different direction. */ if (lc->type != type) { TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; layout_resize_adjust(w, lcchild, type, change); } @@ -506,7 +533,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (change == 0) break; - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; if (change > 0) { layout_resize_adjust(w, lcchild, type, 1); @@ -521,14 +548,118 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, } } -/* Destroy a cell and redistribute the space. */ +/* + * Redistribute space equally among all visible (non-hidden WINDOWPANE) + * children of lcparent in the given direction. Hidden WINDOWPANE leaves + * are skipped; their stored sizes are left untouched. Container children + * have their own children resized proportionally via layout_resize_child_cells. + * + * If all children happen to be hidden (n==0), nothing is done. + */ +void +layout_redistribute_cells(struct window *w, struct layout_cell *lcparent, + enum layout_type type) +{ + struct layout_cell *lc; + u_int n, total, each, rem, i, target; + + /* Count visible cells at this level. */ + n = 0; + TAILQ_FOREACH(lc, &lcparent->cells, entry) { + if (lc->type == LAYOUT_WINDOWPANE && + lc->wp != NULL && + (lc->wp->flags & PANE_HIDDEN)) + continue; + n++; + } + if (n == 0) + return; + + total = (type == LAYOUT_LEFTRIGHT) ? lcparent->sx : lcparent->sy; + if (total + 1 < n) /* can't fit even the minimum borders */ + return; + + /* + * each * n + (n-1) borders = total + * → each = (total - (n-1)) / n, rem = (total - (n-1)) % n + * The first `rem` visible cells get (each+1) to consume the remainder. + */ + each = (total - (n - 1)) / n; + rem = (total - (n - 1)) % n; + + i = 0; + TAILQ_FOREACH(lc, &lcparent->cells, entry) { + if (lc->type == LAYOUT_WINDOWPANE && + lc->wp != NULL && + (lc->wp->flags & PANE_HIDDEN)) + continue; + target = each + (i < rem ? 1 : 0); + if (type == LAYOUT_LEFTRIGHT) + lc->sx = target; + else + lc->sy = target; + if (lc->type != LAYOUT_WINDOWPANE) + layout_resize_child_cells(w, lc); + i++; + } +} + +/* Helper function for layout_cell_get_neighbour. */ +static struct layout_cell * +layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) +{ + struct layout_cell *lcother = lc; + + while (1) { + if (direction) + lcother = TAILQ_NEXT(lcother, entry); + else + lcother = TAILQ_PREV(lcother, layout_cells, entry); + + if (lcother == NULL || layout_cell_is_tiled(lcother)) + return (lcother); + } +} + +/* + * Finds the nearest visible neighbour. A neighbour is a sibling cell drawn + * within the tiled layout. Prefers cells "before" the specified cell. + * This behavior defines how cell dimensions are redistributed when a cell is + * hidden/shown and floated/tiled. + */ +struct layout_cell * +layout_cell_get_neighbour(struct layout_cell *lc) +{ + struct layout_cell *lcother, *lcparent = lc->parent; + int direction = 0; + + if (lcparent == NULL) + return (NULL); + + if (lc == TAILQ_FIRST(&lcparent->cells)) + direction = 1; + + lcother = layout_cell_get_neighbour_direction(lc, direction); + if (lcother == NULL) + lcother = layout_cell_get_neighbour_direction(lc, !direction); + + return lcother; +} + + +/* Destroy a cell and redistribute the space if the cell was tiled. */ void layout_destroy_cell(struct window *w, struct layout_cell *lc, struct layout_cell **lcroot) { - struct layout_cell *lcother = NULL, *lcparent; + struct layout_cell *lcother, *lcparent; + int val; - /* If no parent, this is the last pane in a window. */ + /* + * If no parent, this is either a floating pane or the last + * pane so window close is imminent and there is no need to + * resize anything. + */ lcparent = lc->parent; if (lcparent == NULL) { if (lc->wp != NULL) @@ -537,27 +668,27 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, return; } - if (~lc->flags & LAYOUT_CELL_FLOATING) { - /* Merge the space into the previous or next cell. */ - if (lc == TAILQ_FIRST(&lcparent->cells)) - lcother = TAILQ_NEXT(lc, entry); - else - lcother = TAILQ_PREV(lc, layout_cells, entry); - } - if (lcother != NULL && (~lcother->flags & LAYOUT_CELL_FLOATING)) { - if (lcparent->type == LAYOUT_LEFTRIGHT) { - layout_resize_adjust(w, lcother, lcparent->type, - lc->sx + 1); - } else { - layout_resize_adjust(w, lcother, lcparent->type, - lc->sy + 1); - } + if (!layout_cell_is_tiled(lc)) { + TAILQ_REMOVE(&lcparent->cells, lc, entry); + layout_free_cell(lc); + goto out; } + lcother = layout_cell_get_neighbour(lc); + if (lcother != NULL) { + if (lcparent->type == LAYOUT_LEFTRIGHT) + val = lc->sx + 1; + else + val = lc->sy + 1; + layout_resize_adjust(w, lcother, lcparent->type, val); + } else + layout_hide_cell(w, lcparent); + /* Remove this from the parent's list. */ TAILQ_REMOVE(&lcparent->cells, lc, entry); layout_free_cell(lc); +out: /* * If the parent now has one cell, remove the parent from the tree and * replace it by that cell. @@ -568,10 +699,8 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, lc->parent = lcparent->parent; if (lc->parent == NULL) { - if (~lc->flags & LAYOUT_CELL_FLOATING) { - lc->xoff = 0; - lc->yoff = 0; - } + if (layout_cell_is_tiled(lc)) + layout_set_size(lc, w->sx, w->sy, 0, 0); *lcroot = lc; } else TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry); @@ -580,6 +709,71 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, } } + +/* Hide a cell. Space is redistributed to the nearest neighbour if the cell was + * tiled. + */ +void +layout_hide_cell(struct window *w, struct layout_cell *lc) +{ + struct layout_cell *lcother, *lcparent, *lcchild; + u_int shown_children = 0; + int val; + + if (lc == NULL) + return; + + lcparent = lc->parent; + lc->flags |= LAYOUT_CELL_HIDDEN; + + /* Merge the space into the nearest neighbour. */ + lcother = layout_cell_get_neighbour(lc); + + if (lcother != NULL) { + if (lcparent->type == LAYOUT_LEFTRIGHT) + val = lc->sx + 1; + else + val = lc->sy + 1; + layout_resize_adjust(w, lcother, lcparent->type, val); + } + + if (layout_cell_is_tiled(lc)) + layout_set_size(lc, 0, 0, 0, 0); + + /* If no children are tiled, hide the parent. */ + if (lcparent != NULL) { + TAILQ_FOREACH(lcchild, &lcparent->cells, entry) { + if (layout_cell_is_tiled(lcchild)) { + shown_children = 1; + break; + } + } + if (shown_children == 0) + layout_hide_cell(w, lcparent); + } +} + +/* Show a cell and redistribute the space in tiled cells. */ +void +layout_show_cell(struct window *w, struct layout_cell *lc) +{ + struct layout_cell *lcparent; + + if (lc == NULL) + return; + lcparent = lc->parent; + if (lcparent == NULL) + return; + + /* + * Redistribute the parent's space equally among all visible (non- + * hidden) children, including lc which has just been shown. + * This ensures every pane at this level gets an equal share rather + * than one pane losing most of its space to the restored pane. + */ + layout_redistribute_cells(w, lcparent, lcparent->type); +} + /* Initialize layout for pane. */ void layout_init(struct window *w, struct window_pane *wp) @@ -619,7 +813,7 @@ layout_resize(struct window *w, u_int sx, u_int sy) * out proportionately - this should leave the layout fitting the new * window size. */ - if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) + if (lc->type == LAYOUT_WINDOWPANE && !layout_cell_is_tiled(lc)) return; xchange = sx - lc->sx; xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT); @@ -988,7 +1182,7 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc) count = 0; previous = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; count++; if (lc->type == LAYOUT_LEFTRIGHT) @@ -1008,7 +1202,7 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc) /* Resize children into the new size. */ idx = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->flags & LAYOUT_CELL_FLOATING) + if (!layout_cell_is_tiled(lcchild)) continue; if (lc->type == LAYOUT_TOPBOTTOM) { lcchild->sx = lc->sx; @@ -1215,11 +1409,16 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, * layout_assign_pane before much else happens! */ struct layout_cell * -layout_floating_pane(struct window *w, u_int sx, u_int sy, int ox, int oy) +layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, + u_int sy, int ox, int oy) { - struct layout_cell *lc = w->layout_root, *lcnew, *lcparent; + struct layout_cell *lc = wp->layout_cell, *lcnew, *lcparent; - if (lc->type == LAYOUT_WINDOWPANE) { + if (lc == NULL) + lc = w->layout_root; + lcparent = lc->parent; + + if (lcparent == NULL) { /* * Adding a pane to a root that doesn't have a container. Must * create and insert a new root. @@ -1232,11 +1431,10 @@ layout_floating_pane(struct window *w, u_int sx, u_int sy, int ox, int oy) /* Insert the old cell. */ lc->parent = lcparent; TAILQ_INSERT_HEAD(&lcparent->cells, lc, entry); - } else - lcparent = w->layout_root; + } lcnew = layout_create_cell(lcparent); - TAILQ_INSERT_TAIL(&lcparent->cells, lcnew, entry); + TAILQ_INSERT_AFTER(&lcparent->cells, lc, lcnew, entry); lcnew->flags |= LAYOUT_CELL_FLOATING; layout_set_size(lcnew, sx, sy, ox, oy); @@ -1411,10 +1609,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, /* Get a new floating cell. */ struct layout_cell * layout_get_floating_cell(struct cmdq_item *item, struct args *args, - struct window *w, __unused struct window_pane *wp, char **cause) + struct window *w, struct window_pane *wp, char **cause) { struct layout_cell *lcnew; - int sx = w->sx / 2, sy = w->sy / 4; + u_int sx = w->sx / 2, sy = w->sy / 4; int ox = INT_MAX, oy = INT_MAX; if (args_has(args, 'x')) { @@ -1472,6 +1670,6 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, return (NULL); } - lcnew = layout_floating_pane(w, sx, sy, ox, oy); + lcnew = layout_floating_pane(w, wp, sx, sy, ox, oy); return (lcnew); } diff --git a/options-table.c b/options-table.c index c2ebf64e6..c3bdca0eb 100644 --- a/options-table.c +++ b/options-table.c @@ -1246,6 +1246,28 @@ const struct options_table_entry options_table[] = { .text = "Character used to fill unused parts of window." }, + { .name = "floating-pane-border-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, + .default_str = "default", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Default border style for floating panes. " + "Overrides pane-border-style for floating panes unless " + "a per-pane style is set." + }, + + { .name = "floating-pane-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, + .default_str = "default", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Default content style for floating panes. " + "Overrides window-style for floating panes unless " + "a per-pane style is set." + }, + { .name = "main-pane-height", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/screen-redraw.c b/screen-redraw.c index 8df35aa35..80ee85a1c 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -610,6 +610,8 @@ screen_redraw_make_pane_status(struct client *c, struct window_pane *wp, else border_option = "pane-border-style"; style_apply(&gc, wp->options, border_option, ft); + if (window_pane_is_floating(wp)) + style_add(&gc, wp->options, "floating-pane-border-style", ft); fmt = options_get_string(wp->options, "pane-border-format"); expanded = format_expand_time(ft, fmt); diff --git a/screen-write.c b/screen-write.c index cfc56250c..df7ea0f9b 100644 --- a/screen-write.c +++ b/screen-write.c @@ -146,6 +146,9 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c) if (wp->layout_cell == NULL) return (0); + if (wp->flags & PANE_HIDDEN) + return (0); + if (wp->flags & (PANE_REDRAW|PANE_DROP)) return (-1); if (c->flags & CLIENT_REDRAWPANES) { @@ -2065,8 +2068,11 @@ screen_write_collect_clear(struct screen_write_ctx *ctx, u_int y, u_int n) struct screen_write_cline *cl; u_int i; + log_debug("%s: clearing rows %u..%u", __func__, y, y + n - 1); for (i = y; i < y + n; i++) { cl = &ctx->s->write_list[i]; + if (!TAILQ_EMPTY(&cl->items)) + log_debug("%s: row %u had items!", __func__, i); TAILQ_CONCAT(&screen_write_citem_freelist, &cl->items, entry); } } @@ -2081,8 +2087,8 @@ screen_write_collect_scroll(struct screen_write_ctx *ctx, u_int bg) char *saved; struct screen_write_citem *ci; - log_debug("%s: at %u,%u (region %u-%u)", __func__, s->cx, s->cy, - s->rupper, s->rlower); + log_debug("%s: at %u,%u (region %u-%u) cy=%u", __func__, s->cx, s->cy, + s->rupper, s->rlower, s->cy); screen_write_collect_clear(ctx, s->rupper, 1); saved = ctx->s->write_list[s->rupper].data; @@ -2907,9 +2913,14 @@ screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, } screen_write_collect_flush(ctx, 0, __func__); - screen_write_initctx(ctx, &ttyctx, 0, 0); + screen_write_initctx(ctx, &ttyctx, 0, 1); ttyctx.image = image_store(s, si); + if ((ttyctx.flags & TTY_CTX_PANE_OBSCURED) && ctx->wp != NULL) { + ctx->wp->flags |= PANE_REDRAW; + return; + } + tty_write(tty_cmd_sixelimage, &ttyctx); screen_write_cursormove(ctx, 0, cy + y, 0); diff --git a/server-client.c b/server-client.c index 4a25c82b4..9838ca1a6 100644 --- a/server-client.c +++ b/server-client.c @@ -1497,8 +1497,9 @@ server_client_loop(void) { struct client *c; struct window *w; - struct window_pane *wp; + struct window_pane *wp, *twp; struct window_mode_entry *wme; + u_int bit; /* Check for window resize. This is done before redrawing. */ RB_FOREACH(w, windows, &windows) @@ -1536,6 +1537,42 @@ server_client_loop(void) server_client_check_pane_resize(wp); server_client_check_pane_buffer(wp); } + /* + * If PANE_REDRAW was set during buffer processing + * above, check_redraw has already run for this + * iteration and will not see it. Defer the redraw + * to the next iteration via CLIENT_REDRAWPANES so + * screen_redraw_pane fires once the grid is complete + * (e.g. after the shell prompt has been written). + */ + if (wp->flags & PANE_REDRAW) { + bit = 0; + TAILQ_FOREACH(twp, &w->panes, entry) { + if (twp == wp) { + TAILQ_FOREACH(c, &clients, + entry) { + if (c->session == NULL || + c->session->curw == + NULL || + c->session->curw->window + != w) + continue; + if (bit < 64) { + c->redraw_panes + |= (1ULL + << bit); + c->flags |= + CLIENT_REDRAWPANES; + } else + c->flags |= + CLIENT_REDRAWWINDOW; + } + break; + } + if (++bit == 64) + break; + } + } wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR); } check_window_name(w); diff --git a/tmux.1 b/tmux.1 index c441e3d2b..fe2e8a256 100644 --- a/tmux.1 +++ b/tmux.1 @@ -292,6 +292,8 @@ Rename the current session. Split the current pane into two, left and right. .It & Kill the current window. +.It @ +Toggle the current pane between floating and tiled. .It \[aq] Prompt for a window index to select. .It \&( @@ -3102,6 +3104,41 @@ zooms the pane. The .Ic find-window command works only if at least one client is attached. +.Tg floatp +.It Xo Ic float\-pane +.Op Fl h Ar height +.Op Fl w Ar width +.Op Fl x Ar x +.Op Fl y Ar y +.Op Fl t Ar target\-pane +.Xc +Lift +.Ar target\-pane +out of the tiled layout and make it a floating pane. +The +.Fl w +and +.Fl h +options set the width and height of the floating pane in columns and lines +respectively; the default is half the window width and height. +The +.Fl x +and +.Fl y +options set the position of the upper-left corner of the pane; +if omitted, new floating panes are cascaded from the top-left of the window. +If +.Fl x , +.Fl y , +.Fl w , +and +.Fl h +are all omitted and the pane was previously returned to the tiled layout +with +.Ic tile\-pane , +its last floating position and size are restored. +The pane must not already be floating or hidden, and the window must not +be zoomed. .Tg joinp .It Xo Ic join\-pane .Op Fl bdfhv @@ -3300,6 +3337,23 @@ or (time). .Fl r reverses the sort order. +.Tg hidep +.It Xo Ic hide\-pane +.Op Fl a +.Op Fl t Ar target\-pane +.Xc +.D1 Pq alias: Ic hidep +Hide +.Ar target\-pane +from the tiled layout without closing it. +The pane continues to run but is no longer visible and does not occupy any +screen space. +Hidden panes are shown in the status line and can be restored with +.Ic show\-pane . +With +.Fl a , +all visible panes in the window are hidden. +The pane must not already be hidden. .Tg movep .It Xo Ic move\-pane .Op Fl bdfhv @@ -3933,6 +3987,31 @@ is omitted and a marked pane is present (see .Ic select\-pane .Fl m ) , the window containing the marked pane is used rather than the current window. +.Tg tilep +.It Xo Ic tile\-pane +.Op Fl t Ar target\-pane +.Xc +Insert a floating +.Ar target\-pane +back into the tiled layout. +The pane is placed by splitting the active tiled pane (or the most recently +active tiled pane, or any visible tiled pane if none is active). +The current floating position and size are saved so they can be restored by +a subsequent +.Ic float\-pane +command with no geometry options. +The pane must be floating and the window must not be zoomed. +.Tg showp +.It Xo Ic show\-pane +.Op Fl t Ar target\-pane +.Xc +.D1 Pq alias: Ic showp +Restore a hidden +.Ar target\-pane +to the tiled layout. +Space is redistributed equally among all visible panes at the same layout +level after the pane is restored. +The pane must be hidden. .Tg unlinkw .It Xo Ic unlink\-window .Op Fl k @@ -5348,6 +5427,42 @@ Set clock hour format. .It Ic fill\-character Ar character Set the character used to fill areas of the terminal unused by a window. .Pp +.It Ic floating\-pane\-border\-style Ar style +Set the default border style for all floating panes in the window. +This overrides +.Ic pane-border-style +for floating panes. +A per-pane style set with +.Fl S +on the +.Ic new\-pane +command or with +.Ic set-option Fl p +takes priority over this option. +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.Pp +.It Ic floating\-pane\-style Ar style +Set the default content style for all floating panes in the window. +This overrides +.Ic window-style +for floating panes. +A per-pane style set with +.Fl s +on the +.Ic new\-pane +command or with +.Ic select-pane Fl P +takes priority over this option. +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.Pp .It Ic main\-pane\-height Ar height .It Ic main\-pane\-width Ar width Set the width or height of the main (left or top) pane in the @@ -6713,6 +6828,7 @@ The following variables are available, where appropriate: .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" +.It Li "pane_hidden_flag" Ta "" Ta "1 if pane is hidden" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" .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" diff --git a/tmux.h b/tmux.h index c9113be6a..d2179cf3b 100644 --- a/tmux.h +++ b/tmux.h @@ -1272,6 +1272,7 @@ struct window_pane { int yoff; int flags; + int saved_flags; #define PANE_REDRAW 0x1 #define PANE_DROP 0x2 #define PANE_FOCUSED 0x4 @@ -1288,6 +1289,14 @@ struct window_pane { #define PANE_THEMECHANGED 0x2000 #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 +#define PANE_HIDDEN 0x20000 +#define PANE_SAVED_FLOAT 0x80000 /* saved_float_* fields are valid */ + + /* Last floating position/size, saved when the pane is tiled. */ + int saved_float_xoff; + int saved_float_yoff; + u_int saved_float_sx; + u_int saved_float_sy; u_int sb_slider_y; u_int sb_slider_h; @@ -1482,7 +1491,8 @@ TAILQ_HEAD(layout_cells, layout_cell); struct layout_cell { enum layout_type type; -#define LAYOUT_CELL_FLOATING 0x1 +#define LAYOUT_CELL_FLOATING 0x1 +#define LAYOUT_CELL_HIDDEN 0x2 int flags; struct layout_cell *parent; @@ -3526,6 +3536,10 @@ void layout_free_cell(struct layout_cell *); void layout_print_cell(struct layout_cell *, const char *, u_int); void layout_destroy_cell(struct window *, struct layout_cell *, struct layout_cell **); +void layout_hide_cell(struct window *, struct layout_cell *); +void layout_show_cell(struct window *, struct layout_cell *); +void layout_redistribute_cells(struct window *, struct layout_cell *, + enum layout_type); void layout_resize_layout(struct window *, struct layout_cell *, enum layout_type, int, int); struct layout_cell *layout_search_by_border(struct layout_cell *, u_int, u_int); @@ -3534,9 +3548,11 @@ void layout_make_leaf(struct layout_cell *, struct window_pane *); void layout_make_node(struct layout_cell *, enum layout_type); void layout_fix_zindexes(struct window *, struct layout_cell *); void layout_fix_offsets(struct window *); +int layout_cell_is_tiled(struct layout_cell *); void layout_fix_panes(struct window *, struct window_pane *); void layout_resize_adjust(struct window *, struct layout_cell *, enum layout_type, int); +struct layout_cell *layout_cell_get_neighbour(struct layout_cell *); void layout_init(struct window *, struct window_pane *); void layout_free(struct window *); void layout_resize(struct window *, u_int, u_int); @@ -3552,8 +3568,8 @@ void layout_assign_pane(struct layout_cell *, struct window_pane *, int); struct layout_cell *layout_split_pane(struct window_pane *, enum layout_type, int, int); -struct layout_cell *layout_floating_pane(struct window *, u_int, u_int, int, - int); +struct layout_cell *layout_floating_pane(struct window *, struct window_pane *, + u_int, u_int, int, int); void layout_close_pane(struct window_pane *); int layout_spread_cell(struct window *, struct layout_cell *); void layout_spread_out(struct window_pane *); diff --git a/tty.c b/tty.c index cd416a5ed..1c515d68a 100644 --- a/tty.c +++ b/tty.c @@ -2133,20 +2133,169 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx) } #ifdef ENABLE_SIXEL +/* + * Render one sub-rectangle of a sixel image at tty position (dst_x, dst_y). + * Caller must have set tty->flags |= TTY_NOBLOCK and called tty_region_off + * and tty_margin_off. After the call, tty->cx and tty->cy are UINT_MAX + * because the cursor position after a sixel DCS sequence is terminal-defined. + */ +static void +tty_sixel_subrect(struct tty *tty, struct sixel_image *si, + u_int src_i, u_int src_j, u_int src_rx, u_int src_ry, + u_int dst_x, u_int dst_y) +{ + struct sixel_image *sub; + char *data; + size_t size; + + if (src_rx == 0 || src_ry == 0) + return; + sub = sixel_scale(si, tty->xpixel, tty->ypixel, + src_i, src_j, src_rx, src_ry, 0); + if (sub == NULL) + return; + data = sixel_print(sub, si, &size); + sixel_free(sub); + if (data == NULL) + return; + tty_cursor(tty, dst_x, dst_y); + tty_add(tty, data, size); + tty->cx = tty->cy = UINT_MAX; + free(data); +} + +/* + * Render the clamped region of sixel image si, split into sub-rectangles + * that avoid cells obscured by floating panes above this one. + * + * Row breakpoints are computed from each floating pane's top/bottom border + * row. Between breakpoints the visible column ranges are constant, so each + * strip is rendered with one set of sub-rectangles. When no floating panes + * overlap the sixel the loop produces a single strip and one sub-rect equal + * to the full clamped rectangle. + * + * i, j : source cell offset (top-left of visible portion after viewport clip) + * rx, ry: rendered size in cells + * x, y : tty destination position + */ +static void +tty_sixelimage_draw(struct tty *tty, const struct tty_ctx *ctx, + struct sixel_image *si, u_int i, u_int j, u_int rx, u_int ry, + u_int x, u_int y) +{ + struct window_pane *wp, *fp; + struct window *w; + struct visible_ranges *vr; + struct visible_range *ri; + u_int sixel_wx0, sixel_wx1, sixel_y0; + u_int breaks[64], nb, b, s, tmp; + u_int strip_start, strip_end, strip_h; + u_int seg_wx0, seg_wx1, seg_w; + u_int src_i2, src_j2, dst_x2; + int fp_tb, fp_bb1, r_top, r_bot; + + tty->flags |= TTY_NOBLOCK; + tty_region_off(tty); + tty_margin_off(tty); + + wp = ctx->arg; + if (wp == NULL) { + /* Overlay/popup: no pane context, render the full clamped rect. */ + tty_sixel_subrect(tty, si, i, j, rx, ry, x, y); + tty_invalidate(tty); + return; + } + + /* + * sixel_y0 is the window y-coordinate of the first rendered row. + * Derived from tty_clamp_area: y = ctx->yoff + ocy + j - ctx->woy, + * so y + ctx->woy = ctx->yoff + ocy + j. + */ + w = wp->window; + sixel_wx0 = x + ctx->wox; + sixel_wx1 = sixel_wx0 + rx; + sixel_y0 = y + ctx->woy; + + nb = 0; + breaks[nb++] = 0; + breaks[nb++] = ry; + + TAILQ_FOREACH(fp, &w->z_index, zentry) { + if (!window_pane_is_floating(fp)) + continue; + fp_tb = (int)((fp->yoff > 0) ? fp->yoff - 1 : 0); + fp_bb1 = (int)fp->yoff + (int)fp->sy + 1; + r_top = fp_tb - (int)sixel_y0; + r_bot = fp_bb1 - (int)sixel_y0; + if (r_top > 0 && (u_int)r_top < ry && nb < 62) + breaks[nb++] = (u_int)r_top; + if (r_bot > 0 && (u_int)r_bot < ry && nb < 62) + breaks[nb++] = (u_int)r_bot; + } + + /* Sort breakpoints (insertion sort; small array). */ + for (b = 1; b < nb; b++) { + tmp = breaks[b]; + s = b; + while (s > 0 && breaks[s - 1] > tmp) { + breaks[s] = breaks[s - 1]; + s--; + } + breaks[s] = tmp; + } + + if (nb > 2) + log_debug("%s: sixel %%%u clipped around floating panes", + __func__, wp->id); + + for (b = 0; b + 1 < nb; b++) { + strip_start = breaks[b]; + strip_end = breaks[b + 1]; + if (strip_end == strip_start) + continue; + strip_h = strip_end - strip_start; + + vr = screen_redraw_get_visible_ranges(wp, ctx->xoff, + sixel_y0 + strip_start, wp->sx, NULL); + + for (s = 0; s < vr->used; s++) { + ri = &vr->ranges[s]; + if (ri->nx == 0) + continue; + seg_wx0 = ri->px; + seg_wx1 = ri->px + ri->nx; + if (seg_wx0 < sixel_wx0) + seg_wx0 = sixel_wx0; + if (seg_wx1 > sixel_wx1) + seg_wx1 = sixel_wx1; + if (seg_wx1 <= seg_wx0) + continue; + seg_w = seg_wx1 - seg_wx0; + src_i2 = i + (seg_wx0 - sixel_wx0); + src_j2 = j + strip_start; + dst_x2 = seg_wx0 - ctx->wox; + tty_sixel_subrect(tty, si, src_i2, src_j2, + seg_w, strip_h, dst_x2, y + strip_start); + } + } + + tty_invalidate(tty); +} + void tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) { struct image *im = ctx->image; struct sixel_image *si = im->data; - struct sixel_image *new; char *data; size_t size; u_int cx = ctx->ocx, cy = ctx->ocy, sx, sy; u_int i, j, x, y, rx, ry; - int fallback = 0; + int fallback; + fallback = 0; if ((~tty->term->flags & TERM_SIXEL) && - !tty_term_has(tty->term, TTYC_SXL)) + !tty_term_has(tty->term, TTYC_SXL)) fallback = 1; if (tty->xpixel == 0 || tty->ypixel == 0) fallback = 1; @@ -2157,30 +2306,20 @@ tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx) return; log_debug("%s: clamping to %u,%u-%u,%u", __func__, i, j, rx, ry); - if (fallback == 1) { + if (fallback) { data = xstrdup(im->fallback); size = strlen(data); - } else { - new = sixel_scale(si, tty->xpixel, tty->ypixel, i, j, rx, ry, 0); - if (new == NULL) - return; - - data = sixel_print(new, si, &size); - } - if (data != NULL) { - log_debug("%s: %zu bytes: %s", __func__, size, data); + tty->flags |= TTY_NOBLOCK; tty_region_off(tty); tty_margin_off(tty); tty_cursor(tty, x, y); - - tty->flags |= TTY_NOBLOCK; tty_add(tty, data, size); tty_invalidate(tty); free(data); + return; } - if (fallback == 0) - sixel_free(new); + tty_sixelimage_draw(tty, ctx, si, i, j, rx, ry, x, y); } #endif @@ -3038,9 +3177,13 @@ tty_style_changed(struct window_pane *wp) tty_window_default_style(&wp->cached_active_gc, wp); style_add(&wp->cached_active_gc, oo, "window-active-style", ft); + if (window_pane_is_floating(wp)) + style_add(&wp->cached_active_gc, oo, "floating-pane-style", ft); tty_window_default_style(&wp->cached_gc, wp); style_add(&wp->cached_gc, oo, "window-style", ft); + if (window_pane_is_floating(wp)) + style_add(&wp->cached_active_gc, oo, "floating-pane-style", ft); format_free(ft); } diff --git a/window-tree.c b/window-tree.c index 42fb05c43..05b174887 100644 --- a/window-tree.c +++ b/window-tree.c @@ -270,7 +270,7 @@ window_tree_build_window(struct session *s, struct winlink *wl, if (data->type == WINDOW_TREE_SESSION || data->type == WINDOW_TREE_WINDOW) - expanded = 0; + expanded = window_has_floating_panes(wl->window); else expanded = 1; mti = mode_tree_add(data->data, parent, item, (uint64_t)wl, name, text, diff --git a/window.c b/window.c index 627a362f5..8d40cf964 100644 --- a/window.c +++ b/window.c @@ -560,6 +560,20 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) w->active->active_point = next_active_point++; w->active->flags |= PANE_CHANGED; + if (wp->flags & PANE_HIDDEN) { + wp->flags &= ~PANE_HIDDEN; + if (w->layout_root != NULL && wp->saved_layout_cell != NULL) { + wp->layout_cell = wp->saved_layout_cell; + wp->saved_layout_cell = NULL; + layout_show_cell(w, wp->layout_cell); + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + } + } + notify_window("window-layout-changed", w); + server_redraw_window(w); + + if (options_get_number(global_options, "focus-events")) { window_pane_update_focus(lastwp); window_pane_update_focus(w->active); @@ -732,8 +746,6 @@ window_zoom(struct window_pane *wp) if (w->flags & WINDOW_ZOOMED) return (-1); - if (window_count_panes(w, 1) == 1) - return (-1); if (w->active != wp) window_set_active_pane(w, wp, 1); @@ -837,26 +849,41 @@ window_add_pane(struct window *w, struct window_pane *other, u_int hlimit, void window_lost_pane(struct window *w, struct window_pane *wp) { + struct window_pane *wpp, *twpp; + log_debug("%s: @%u pane %%%u", __func__, w->id, wp->id); if (wp == marked_pane.wp) server_clear_marked(); window_pane_stack_remove(&w->last_panes, wp); - if (wp == w->active) { - w->active = TAILQ_FIRST(&w->last_panes); - if (w->active == NULL) { - w->active = TAILQ_PREV(wp, window_panes, entry); - if (w->active == NULL) - w->active = TAILQ_NEXT(wp, entry); - } - if (w->active != NULL) { - window_pane_stack_remove(&w->last_panes, w->active); - w->active->flags |= PANE_CHANGED; - notify_window("window-pane-changed", w); - window_update_focus(w); + if (wp != w->active) + return; + + /* Try to find a good fit. */ + wpp = TAILQ_FIRST(&w->last_panes); + if (wpp == NULL || wpp->flags & PANE_HIDDEN) { + wpp = TAILQ_PREV(wp, window_panes, entry); + if (wpp == NULL || wpp->flags & PANE_HIDDEN) + wpp = TAILQ_NEXT(wp, entry); + } + /* Try to find any fit. */ + if (wpp == NULL || (wpp->flags & PANE_HIDDEN)) { + TAILQ_FOREACH_SAFE(wpp, &w->panes, entry, twpp) { + if (wpp != wp && (~wpp->flags & PANE_HIDDEN)) + break; } } + if (wpp != NULL && (wpp->flags & PANE_HIDDEN)) + wpp = NULL; + + w->active = wpp; + if (w->active != NULL) { + window_pane_stack_remove(&w->last_panes, w->active); + w->active->flags |= PANE_CHANGED; + notify_window("window-pane-changed", w); + window_update_focus(w); + } } void @@ -1017,6 +1044,9 @@ window_pane_printable_flags(struct window_pane *wp) flags[pos++] = 'Z'; if (window_pane_is_floating(wp)) flags[pos++] = 'F'; + if (wp->flags & PANE_HIDDEN) + flags[pos++] = 'm'; + flags[pos] = '\0'; return (flags); } @@ -1431,8 +1461,10 @@ window_pane_key(struct window_pane *wp, struct client *c, struct session *s, int window_pane_visible(struct window_pane *wp) { - if (~wp->window->flags & WINDOW_ZOOMED) + if (~wp->window->flags & WINDOW_ZOOMED && + ~wp->flags & PANE_HIDDEN) return (1); + return (wp == wp->window->active); }