Skip to content

Window.from_window_id() / Window.refresh() report the wrong window_index for a window linked twice into one session #717

Description

@tony

Bug

tmux permits linking one window into the same session at two indexes. Asked for that window's window_index, libtmux answers with the last winlink in index order; tmux answers with a different one. The two disagree.

This is the "last row wins" defect of #710, surviving in a case #713 does not reach: #710 is about a window linked across several sessions, and #713 fixes it by scoping the listing with -t. But a window linked twice into one session still produces two rows inside that -t scope, so the survivor loop still overwrites.

Pane.from_pane_id() is unaffected — list-panes emits each pane once.

Reproduction

$ tmux -L dupwin new-session -d -s dup
$ tmux -L dupwin link-window -d -s @0 -t dup:5
$ tmux -L dupwin select-window -t dup:1
tmux list-windows -t @0 -F '#{window_id} index=#{window_index}'
  @0  index=1
  @0  index=5

tmux display-message -p -t @0 '#{window_index}'   ->  1     <- tmux's own answer

libtmux  Window.from_window_id(server, "@0").window_index    ->  5     <- WRONG
libtmux  Window.refresh() then .window_index                 ->  5     <- WRONG
libtmux  Pane.from_pane_id(server, "%0").window_index        ->  1     <- correct

Reproduces identically on master (via list-windows -a) and on the -t-scoped lookups of #713, so it is not a regression — but #713 fixes the Pane side and leaves the Window side, which makes the asymmetry newly visible.

Root cause

Two rows, one id. link-window adds a second winlink for the same window in the same session. The winlinks RB-tree is keyed by index (window.c#L87-L90), so list-windows walks them in ascending index order and emits @0 twice.

fetch_obj keeps the last match. neo.py#L793-L796 iterates every row and overwrites, with no break:

obj = None
for _obj in obj_formatters_filtered:
    if _obj.get(obj_key) == obj_id:
        obj = _obj

So libtmux reports the highest-indexed winlink.

tmux does not answer that way. cmd_find_best_winlink_with_window() prefers the session's current window if it holds the target, and otherwise takes the first winlink in the RB-tree — i.e. the lowest index (cmd-find.c#L209-L231):

wl = NULL;
if (fs->s->curw != NULL && fs->s->curw->window == fs->w)
        wl = fs->s->curw;
else {
        RB_FOREACH(wl_loop, winlinks, &fs->s->windows) {
                if (wl_loop->window == fs->w) {
                        wl = wl_loop;
                        break;
                }
        }
}

Why there is no one-line fix

Neither survivor rule matches tmux, so swapping keep-last for keep-first is not correct either:

state tmux says keep-first keep-last
curw is the index-1 winlink 1 1 5
curw is the index-5 winlink 5 1 5

tmux's rule depends on curw, which no list-windows row carries. Reproducing it in Python would mean re-implementing cmd_find — the thing #713 exists to stop doing.

The honest fix is to ask tmux to collapse the edge, as -t already does for panes: resolve the window through display-message -p -t @N with the required format tokens, which returns exactly one row stamped with the winlink tmux would act on. That means teaching neo.fetch_objs a non-list-* subcommand, which is a refactor rather than a patch — hence a separate issue.

Scope

Only fields that belong to the winlink rather than the window are wrong: window_index, and anything derived from it (window_active, the session:index target form). The window_id is right, and the session_id is right too — both rows carry the same one — so the session-attribution guarantees of #713 hold even here.

The underlying modelling gap is the same one behind #716: a window_id identifies a window, but a row identifies a winlink, and libtmux's object layer assumes the two are one-to-one.

Related

Environment

  • libtmux: 0.61.0
  • tmux: 3.7b
  • Python: 3.13

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions