feat: reset tmux window indexes
This commit is contained in:
@@ -116,6 +116,15 @@ class WindowInfo:
|
|||||||
last_activity: int
|
last_activity: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WindowIndex:
|
||||||
|
"""Stable tmux window identity plus its current numeric index."""
|
||||||
|
|
||||||
|
window_id: str
|
||||||
|
index: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class TmuxRunner:
|
class TmuxRunner:
|
||||||
"""Low-level async tmux command wrapper. All operations target windows within a single session."""
|
"""Low-level async tmux command wrapper. All operations target windows within a single session."""
|
||||||
|
|
||||||
@@ -231,6 +240,103 @@ class TmuxRunner:
|
|||||||
flags.extend(["-e", f"{key}={value}"])
|
flags.extend(["-e", f"{key}={value}"])
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
async def _list_window_indexes(self) -> list[WindowIndex] | None:
|
||||||
|
"""Return window ids, indexes, and names for this tmux session.
|
||||||
|
|
||||||
|
None means tmux failed or returned malformed data; an empty list means
|
||||||
|
the session listed successfully but has no windows.
|
||||||
|
"""
|
||||||
|
rc, stdout, err = await self._exec(
|
||||||
|
"list-windows",
|
||||||
|
"-t",
|
||||||
|
self.session_name,
|
||||||
|
"-F",
|
||||||
|
"#{window_id}\t#{window_index}\t#{window_name}",
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
log.error("list-windows failed for %s: %s", self.session_name, err)
|
||||||
|
return None
|
||||||
|
|
||||||
|
windows: list[WindowIndex] = []
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.split("\t", 2)
|
||||||
|
if len(parts) != 3:
|
||||||
|
log.error("Malformed list-windows row: %r", line)
|
||||||
|
return None
|
||||||
|
window_id, index_text, name = parts
|
||||||
|
try:
|
||||||
|
index = int(index_text)
|
||||||
|
except ValueError:
|
||||||
|
log.error("Malformed window index in row: %r", line)
|
||||||
|
return None
|
||||||
|
windows.append(WindowIndex(window_id=window_id, index=index, name=name))
|
||||||
|
return windows
|
||||||
|
|
||||||
|
async def _move_window_id_to_index(self, window_id: str, index: int) -> bool:
|
||||||
|
rc, _, err = await self._exec(
|
||||||
|
"move-window",
|
||||||
|
"-s",
|
||||||
|
window_id,
|
||||||
|
"-t",
|
||||||
|
f"{self.session_name}:{index}",
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
log.error("move-window failed for %s -> %s: %s", window_id, index, err)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def reset_window_indexes(self, home_window: str) -> bool:
|
||||||
|
"""Compact tmux window indexes while keeping the home window at 0.
|
||||||
|
|
||||||
|
The operation changes tmux indexes only. It preserves window names,
|
||||||
|
panes, processes, hqt database rows, and harness session ids.
|
||||||
|
"""
|
||||||
|
windows = await self._list_window_indexes()
|
||||||
|
if windows is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
home = next((window for window in windows if window.name == home_window), None)
|
||||||
|
if home is None:
|
||||||
|
log.error(
|
||||||
|
"Home window %r not found in session %s",
|
||||||
|
home_window,
|
||||||
|
self.session_name,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
non_home = sorted(
|
||||||
|
(window for window in windows if window.window_id != home.window_id),
|
||||||
|
key=lambda window: window.index,
|
||||||
|
)
|
||||||
|
desired: list[tuple[WindowIndex, int]] = [(home, 0)]
|
||||||
|
desired.extend(
|
||||||
|
(window, index) for index, window in enumerate(non_home, start=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if all(window.index == target_index for window, target_index in desired):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Move every window out of the target range before placing final indexes.
|
||||||
|
# This avoids move-window failures when a destination index is occupied.
|
||||||
|
temp_start = max(window.index for window in windows) + len(windows)
|
||||||
|
for offset, window in enumerate(
|
||||||
|
sorted(windows, key=lambda item: item.index),
|
||||||
|
start=1,
|
||||||
|
):
|
||||||
|
if not await self._move_window_id_to_index(
|
||||||
|
window.window_id,
|
||||||
|
temp_start + offset,
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for window, target_index in desired:
|
||||||
|
if not await self._move_window_id_to_index(window.window_id, target_index):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
async def new_window(
|
async def new_window(
|
||||||
self,
|
self,
|
||||||
window_name: str,
|
window_name: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user