Remembering window pane focus in tmux

Suraj N. Kurapati

  1. Problem
    1. Approach
      1. Solution

        Problem

        Having switched from the wmii window manager to the tmux terminal multiplexer as my primary “desktop environment” nearly a year ago, I have found tmux’s inability to (1) remember which pane was last focused within a split-window and to (2) restore focus to that pane when revisiting that split-window to be its main functional disadvantage.

        To cope, I would manually restore pane focus in tmux after moving focus between different split-windows. This hurt my productivity since I use split-windows heavily and switch focus between them quite frequently.

        Approach

        Recently, I spent several weeknights poring over tmux’s source code in hopes of correcting this problem once and for all. The following patch, which applies cleanly against tmux version 1.8, resulted from all that hard work. I have since contributed it upstream, to the “tmux-users” mailing list.

        This patch makes non-window-pane layout cells remember which of their descendant window panes previously had focus (or was “active” in tmux parlance) so that the focus may be restored later on, when applicable.

        For example, consider the following scenario created by steps 1-5:

            1            2            3            4            5
        
        @@@@@@@@@    +---@@@@@    +---+---+    @@@@@---+    +---+---+
        @       @    |   @   @    |   | Y |    @   @ Y |    |   | Y |
        @   X   @    | X @ Y @    | X @@@@@    @ X @---+    | X @@@@@
        @       @    |   @   @    |   @ Z @    @   @ Z |    |   @ Z @
        @@@@@@@@@    +---@@@@@    +---@@@@@    @@@@@---+    +---@@@@@
        
        1. We run tmux new-window to create pane X.
        2. We run tmux split-window -h to create and focus pane Y.
        3. We run tmux split-window to create and focus pane Z.
        4. We run tmux select-pane -L to focus pane X.
        5. We run tmux select-pane -R to focus pane Z, thanks to this patch. Without this patch, this step would focus pane Y instead of pane Z.

        Although this example illustrates a rather simple scenario, note that this patch can handle more complex (nested cell) layouts just as well.

        Solution

        This is also available as a downloadable Git patch file.

        diff --git a/window.c b/window.c
        index 7678adc..adb67e0 100644
        --- a/window.c
        +++ b/window.c
        @@ -383,6 +383,34 @@ window_resize(struct window *w, u_int sx, u_int sy)
                w->sy = sy;
         }
        
        +/*
        + * Commandeers the transfer of focus from `wp` to `wp2` in order to mark `wp`
        + * so that window_pane_refocus is able to restore focus to it when necessary.
        + */
        +static void
        +window_pane_defocus(struct window_pane *wp, struct window_pane *wp2)
        +{
        +   struct layout_cell *lc, *lc2;
        +
        +   /* remember the target pane's focus in its parent */
        +   wp2->layout_cell->parent->wp = wp2;
        +
        +   if (wp == NULL)
        +       return;
        +
        +   /* remember the source pane's focus in all of its parents up to, but
        +    * not including, the common ancestor of itself and the target pane */
        +   for(lc = wp->layout_cell->parent; lc != NULL; lc = lc->parent) {
        +       /* search for a common ancestor in the target's lineage */
        +       for(lc2 = wp2->layout_cell->parent; lc2 != NULL; lc2 = lc2->parent)
        +           if (lc == lc2)
        +               /* found a common ancestor so stop here */
        +               return;
        +       /* remember the source pane's focus in its uncommon parent */
        +       lc->wp = wp;
        +   }
        +}
        +
         void
         window_set_active_pane(struct window *w, struct window_pane *wp)
         {
        @@ -390,6 +418,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp)
                        return;
                w->last = w->active;
                w->active = wp;
        +   window_pane_defocus(w->last, wp);
                while (!window_pane_visible(w->active)) {
                        w->active = TAILQ_PREV(w->active, window_panes, entry);
                        if (w->active == NULL)
        @@ -704,6 +733,14 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit)
         void
         window_pane_destroy(struct window_pane *wp)
         {
        +   /* forget removed pane's focus in all layout cells that remember it */
        +   struct window_pane *wp2;
        +   RB_FOREACH(wp2, window_pane_tree, &all_window_panes)
        +       if (wp2->layout_cell != NULL &&
        +           wp2->layout_cell->parent != NULL &&
        +           wp2->layout_cell->parent->wp == wp)
        +           wp2->layout_cell->parent->wp = NULL; /* forget pane */
        +
                window_pane_reset_mode(wp);
        
                if (event_initialized(&wp->changes_timer))
        @@ -1110,6 +1147,31 @@ window_pane_search(struct window_pane *wp, const char *searchstr, u_int *lineno)
                return (msg);
         }
        
        +/*
        + * Commandeers the transfer of focus from `wp` to `wp2` in order to restore
        + * focus to a previously focused window pane marked by window_pane_defocus.
        + * If this is not possible, the transfer proceeds as originally intended.
        + */
        +static struct window_pane *
        +window_pane_refocus(struct window_pane *wp, struct window_pane *wp2)
        +{
        +   struct layout_cell *lc;
        +   struct window_pane *fp; /* pane marked by window_pane_defocus */
        +
        +   /* target pane's parent must not be an ancestor of source pane */
        +   for (lc = wp->layout_cell->parent; lc != NULL; lc = lc->parent)
        +       if (lc == wp2->layout_cell->parent)
        +           return (wp2);
        +
        +   /* focused pane, if any, must not be the same as source pane */
        +   lc = wp2->layout_cell->parent;
        +   fp = lc != NULL ? lc->wp : NULL;
        +   if (fp != NULL && fp != wp && window_pane_visible(fp))
        +       return (fp);
        +   else
        +       return (wp2);
        +}
        +
         /* Find the pane directly above another. */
         struct window_pane *
         window_pane_find_up(struct window_pane *wp)
        @@ -1131,7 +1193,7 @@ window_pane_find_up(struct window_pane *wp)
                        if (wp2->yoff + wp2->sy + 1 != top)
                                continue;
                        if (left >= wp2->xoff && left <= wp2->xoff + wp2->sx)
        -           return (wp2);
        +           return window_pane_refocus(wp, wp2);
                }
                return (NULL);
         }
        @@ -1157,7 +1219,7 @@ window_pane_find_down(struct window_pane *wp)
                        if (wp2->yoff != bottom)
                                continue;
                        if (left >= wp2->xoff && left <= wp2->xoff + wp2->sx)
        -           return (wp2);
        +           return window_pane_refocus(wp, wp2);
                }
                return (NULL);
         }
        @@ -1186,7 +1248,7 @@ window_pane_find_left(struct window_pane *wp)
                        if (wp2->xoff + wp2->sx + 1 != left)
                                continue;
                        if (top >= wp2->yoff && top <= wp2->yoff + wp2->sy)
        -           return (wp2);
        +           return window_pane_refocus(wp, wp2);
                }
                return (NULL);
         }
        @@ -1215,7 +1277,7 @@ window_pane_find_right(struct window_pane *wp)
                        if (wp2->xoff != right)
                                continue;
                        if (top >= wp2->yoff && top <= wp2->yoff + wp2->sy)
        -           return (wp2);
        +           return window_pane_refocus(wp, wp2);
                }
                return (NULL);
         }
        

        Updates