From 811604a66377fc71c978a18cdbebc3b280d6be5d Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Sun, 5 Apr 2026 19:04:10 -0400 Subject: [PATCH] Fix bugs with tiling floating panes. --- cmd-minimise-pane.c | 4 ++ cmd-tile-float-pane.c | 124 ++++++++++++++++++++--------------- layout.c | 147 +++++++++++++++++++++++++++++++++++------- tmux.1 | 28 ++++++++ tmux.h | 2 + 5 files changed, 230 insertions(+), 75 deletions(-) diff --git a/cmd-minimise-pane.c b/cmd-minimise-pane.c index 14712726..89e47c4e 100644 --- a/cmd-minimise-pane.c +++ b/cmd-minimise-pane.c @@ -143,6 +143,10 @@ cmd_minimise_pane_minimise(struct window *w, struct window_pane *wp) { struct window_pane *wp2; + /* Ignore if already minimised to prevent double-redistribution. */ + if (wp->flags & PANE_MINIMISED) + return (CMD_RETURN_NORMAL); + wp->flags |= PANE_MINIMISED; window_deactivate_pane(w, wp, 1); diff --git a/cmd-tile-float-pane.c b/cmd-tile-float-pane.c index 58129289..6f3dd8ca 100644 --- a/cmd-tile-float-pane.c +++ b/cmd-tile-float-pane.c @@ -36,11 +36,6 @@ 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 *); -static enum cmd_retval do_float_pane(struct window *, struct window_pane *, - int, int, u_int, u_int); -static enum cmd_retval do_tile_pane(struct window *, struct window_pane *, - struct cmdq_item *); - const struct cmd_entry cmd_float_pane_entry = { .name = "float-pane", .alias = NULL, @@ -76,7 +71,7 @@ const struct cmd_entry cmd_tile_pane_entry = { * caller's statics. */ static int -parse_float_geometry(struct args *args, struct cmdq_item *item, +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) { @@ -162,6 +157,7 @@ cmd_float_pane_exec(struct cmd *self, struct cmdq_item *item) static int last_x = 0, last_y = 0; int x, y; u_int sx, sy; + struct layout_cell *lc; if (wp->flags & PANE_FLOATING) { cmdq_error(item, "pane is already floating"); @@ -188,40 +184,11 @@ cmd_float_pane_exec(struct cmd *self, struct cmdq_item *item) sx = wp->saved_float_sx; sy = wp->saved_float_sy; } else { - if (parse_float_geometry(args, item, w, &x, &y, &sx, &sy, - &last_x, &last_y) != 0) + if (cmd_float_pane_parse_geometry(args, item, w, &x, &y, &sx, + &sy, &last_x, &last_y) != 0) return (CMD_RETURN_ERROR); } - return (do_float_pane(w, wp, x, y, sx, sy)); -} - -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; - - if (!(wp->flags & PANE_FLOATING)) { - 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); - } - - return (do_tile_pane(w, wp, item)); -} - -static enum cmd_retval -do_float_pane(struct window *w, struct window_pane *wp, int x, int y, - u_int sx, u_int sy) -{ - struct layout_cell *lc; - /* * Remove the pane from the tiled layout tree so neighbours reclaim * the space. layout_close_pane calls layout_destroy_cell which frees @@ -254,10 +221,26 @@ do_float_pane(struct window *w, struct window_pane *wp, int x, int y, } static enum cmd_retval -do_tile_pane(struct window *w, struct window_pane *wp, struct cmdq_item *item) +cmd_tile_pane_exec(struct cmd *self, struct cmdq_item *item) { - struct window_pane *target_wp; + __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_minimised; + + if (!(wp->flags & PANE_FLOATING)) { + 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_minimised = (wp->flags & PANE_MINIMISED) != 0; /* * Save the floating geometry so we can restore it next time this pane @@ -270,37 +253,58 @@ do_tile_pane(struct window *w, struct window_pane *wp, struct cmdq_item *item) wp->saved_float_sy = float_lc->sy; wp->flags |= PANE_SAVED_FLOAT; + /* + * If the pane is also minimised, clear saved_layout_cell before + * freeing the floating cell — otherwise the pointer would dangle. + */ + if (was_minimised) + 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 already NULL */ + layout_free_cell(float_lc); wp->layout_cell = NULL; /* - * Find the best tiled pane to split after: prefer the active pane - * (if tiled), then the most-recently-visited tiled pane, then any - * visible tiled pane. + * Find the best tiled pane to split after, prefer a visible (non- + * minimised) tiled pane. If all tiled panes are minimised, 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 && !(w->active->flags & PANE_FLOATING)) + if (w->active != NULL && !(w->active->flags & PANE_FLOATING) && + !(w->active->flags & PANE_MINIMISED)) target_wp = w->active; if (target_wp == NULL) { - TAILQ_FOREACH(target_wp, &w->last_panes, sentry) { - if (!(target_wp->flags & PANE_FLOATING) && - window_pane_visible(target_wp)) + TAILQ_FOREACH(wpiter, &w->last_panes, sentry) { + if (!(wpiter->flags & (PANE_FLOATING|PANE_MINIMISED)) && + window_pane_visible(wpiter)) { + target_wp = wpiter; break; + } } } if (target_wp == NULL) { - TAILQ_FOREACH(target_wp, &w->panes, entry) { - if (!(target_wp->flags & PANE_FLOATING) && - window_pane_visible(target_wp)) + TAILQ_FOREACH(wpiter, &w->panes, entry) { + if (!(wpiter->flags & (PANE_FLOATING|PANE_MINIMISED)) && + window_pane_visible(wpiter)) { + target_wp = wpiter; break; + } + } + } + /* Fall back to any tiled pane (even minimised) to stay in the tree. */ + if (target_wp == NULL) { + TAILQ_FOREACH(wpiter, &w->panes, entry) { + if (!(wpiter->flags & PANE_FLOATING)) { + target_wp = wpiter; + break; + } } } - if (target_wp != NULL) { lc = layout_split_pane(target_wp, LAYOUT_TOPBOTTOM, -1, 0); if (lc == NULL) @@ -311,6 +315,14 @@ do_tile_pane(struct window *w, struct window_pane *wp, struct cmdq_item *item) 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 @@ -325,11 +337,19 @@ do_tile_pane(struct window *w, struct window_pane *wp, struct cmdq_item *item) layout_make_leaf(lc, wp); } + /* + * If the pane was minimised while floating, record its new tiled cell + * as the saved cell so unminimise can restore it correctly. + */ + if (was_minimised) + wp->saved_layout_cell = wp->layout_cell; + wp->flags &= ~PANE_FLOATING; TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); - window_set_active_pane(w, wp, 1); + if (!(wp->flags & PANE_MINIMISED)) + window_set_active_pane(w, wp, 1); if (w->layout_root != NULL) layout_fix_offsets(w); diff --git a/layout.c b/layout.c index 788497db..b83af8a4 100644 --- a/layout.c +++ b/layout.c @@ -46,6 +46,9 @@ static int layout_set_size_check(struct window *, struct layout_cell *, enum layout_type, int); static void layout_resize_child_cells(struct window *, struct layout_cell *); +static struct layout_cell *layout_active_neighbour(struct layout_cell *, int); +void layout_redistribute_cells(struct window *, struct layout_cell *, + enum layout_type); struct layout_cell * layout_create_cell(struct layout_cell *lcparent) @@ -524,6 +527,92 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, } } +/* + * Return the nearest sibling of lc that is not a minimised WINDOWPANE leaf, + * walking forward (forward=1) or backward (forward=0) in the parent's list. + * Container cells (TOPBOTTOM/LEFTRIGHT) are never skipped. + */ +static struct layout_cell * +layout_active_neighbour(struct layout_cell *lc, int forward) +{ + struct layout_cell *lcother; + + if (forward) + lcother = TAILQ_NEXT(lc, entry); + else + lcother = TAILQ_PREV(lc, layout_cells, entry); + + while (lcother != NULL) { + if (lcother->type != LAYOUT_WINDOWPANE) + return (lcother); /* container — not skipped */ + if (lcother->wp == NULL || + !(lcother->wp->flags & PANE_MINIMISED)) + return (lcother); /* visible leaf */ + /* minimised leaf — keep walking */ + if (forward) + lcother = TAILQ_NEXT(lcother, entry); + else + lcother = TAILQ_PREV(lcother, layout_cells, entry); + } + return (NULL); +} + +/* + * Redistribute space equally among all visible (non-minimised WINDOWPANE) + * children of lcparent in the given direction. Minimised 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 minimised (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_MINIMISED)) + 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_MINIMISED)) + 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++; + } +} + /* Destroy a cell and redistribute the space in tiled cells. */ void layout_destroy_cell(struct window *w, struct layout_cell *lc, @@ -546,10 +635,11 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, /* In tiled layouts, merge the space into the previous or next cell. */ if (lcparent->type != LAYOUT_FLOATING) { - if (lc == TAILQ_FIRST(&lcparent->cells)) - lcother = TAILQ_NEXT(lc, entry); - else - lcother = TAILQ_PREV(lc, layout_cells, entry); + int forward; + forward = (lc == TAILQ_FIRST(&lcparent->cells)) ? 1 : 0; + lcother = layout_active_neighbour(lc, forward); + if (lcother == NULL) + lcother = layout_active_neighbour(lc, !forward); if (lcother != NULL && lcparent->type == LAYOUT_LEFTRIGHT) layout_resize_adjust(w, lcother, lcparent->type, lc->sx + 1); else if (lcother != NULL) @@ -574,6 +664,19 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, lc->parent = lcparent->parent; if (lc->parent == NULL) { lc->xoff = 0; lc->yoff = 0; + /* + * If the sole remaining child is a minimised + * WINDOWPANE, its stored size may be stale (it never + * received the space that was given to the removed + * cell). Restore the full window size so that + * unminimise can reclaim the correct amount. + */ + if (lc->type == LAYOUT_WINDOWPANE && + lc->wp != NULL && + (lc->wp->flags & PANE_MINIMISED)) { + lc->sx = lcparent->sx; + lc->sy = lcparent->sy; + } *lcroot = lc; } else TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry); @@ -595,11 +698,14 @@ layout_minimise_cell(struct window *w, struct layout_cell *lc) return; } - /* 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); + /* Merge the space into the nearest non-minimised sibling. */ + { + int forward; + forward = (lc == TAILQ_FIRST(&lcparent->cells)) ? 1 : 0; + lcother = layout_active_neighbour(lc, forward); + if (lcother == NULL) + lcother = layout_active_neighbour(lc, !forward); + } if (lcother != NULL && lcparent->type == LAYOUT_LEFTRIGHT) layout_resize_adjust(w, lcother, lcparent->type, lc->sx + 1); else if (lcother != NULL) @@ -626,26 +732,21 @@ layout_minimise_cell(struct window *w, struct layout_cell *lc) void layout_unminimise_cell(struct window *w, struct layout_cell *lc) { - struct layout_cell *lcother, *lcparent; + struct layout_cell *lcparent; if (lc == NULL) return; lcparent = lc->parent; - if (lcparent == NULL) { + if (lcparent == NULL || lcparent->type == LAYOUT_FLOATING) return; - } - /* In tiled layouts, merge the space into the previous or next cell. */ - if (lcparent->type != LAYOUT_FLOATING) { - if (lc == TAILQ_FIRST(&lcparent->cells)) - lcother = TAILQ_NEXT(lc, entry); - else - lcother = TAILQ_PREV(lc, layout_cells, entry); - if (lcother != NULL && lcparent->type == LAYOUT_LEFTRIGHT) - layout_resize_adjust(w, lcother, lcparent->type, -(lc->sx + 1)); - else if (lcother != NULL) - layout_resize_adjust(w, lcother, lcparent->type, -(lc->sy + 1)); - } + /* + * Redistribute the parent's space equally among all visible (non- + * minimised) children, including lc which has just been unminimised. + * 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); } void diff --git a/tmux.1 b/tmux.1 index 8b364944..8f0fdc2d 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3192,6 +3192,23 @@ or (time). .Fl r reverses the sort order. +.Tg minp +.It Xo Ic minimise\-pane +.Op Fl a +.Op Fl t Ar target\-pane +.Xc +.D1 Pq alias: Ic minimize\-pane +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. +Minimised panes are shown in the status line and can be restored with +.Ic unminimise\-pane . +With +.Fl a , +all visible panes in the window are minimised. +The pane must not already be minimised. .Tg movep .It Xo Ic move\-pane .Op Fl bdfhv @@ -3833,6 +3850,17 @@ a subsequent .Ic float\-pane command with no geometry options. The pane must be floating and the window must not be zoomed. +.Tg unminp +.It Xo Ic unminimise\-pane +.Op Fl t Ar target\-pane +.Xc +.D1 Pq alias: Ic unminimize\-pane +Restore a minimised +.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 minimised. .Tg unlinkw .It Xo Ic unlink\-window .Op Fl k diff --git a/tmux.h b/tmux.h index 1a2c6449..3b958237 100644 --- a/tmux.h +++ b/tmux.h @@ -3445,6 +3445,8 @@ void layout_destroy_cell(struct window *, struct layout_cell *, struct layout_cell **); void layout_minimise_cell(struct window *, struct layout_cell *); void layout_unminimise_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);