aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <peter@haxx.in>2026-09-29 14:31:26 +0200
committerblasty <peter@haxx.in>2026-09-29 14:31:26 +0200
commit6252b7c6a89043cfee636aa418303c5a86b4d108 (patch)
tree87259b99504789bf1b5a0d7a2ee10c532bc6ba9a
parentSpawn TUI panes in slosh, the multiplexer we actually use (diff)
downloadida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.tar.gz
ida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.tar.xz
ida-tui-6252b7c6a89043cfee636aa418303c5a86b4d108.zip
Put a spawned pane beside US, not beside whoever is looking
The slosh backend worked out "where do I live" from `panes[].focused`. Exactly one pane in a session is focused, it is always in the tab currently on screen, and it belongs to whoever is looking -- so a TUI spawned "beside the agent" landed in whatever tab the human had wandered into, and in a session running several agents it was reliably a stranger's. `apply-layout` jumping the view to the tab it built made it self-inflicted too: one spawn moved the focus the next spawn would read. Identity now comes from four sources, in order, none of which is "whoever has focus": $IDATUI_SLOSH_PANE (an operator saying so), $SLOSH_PANE (slosh telling the pane's program which pane it is), our own /proc ancestry matched against panes[].pid (the answer for an agent's shell-out, which has neither a scrubbed environment nor a controlling tty), and a purpose named by $IDATUI_ORIGIN_PURPOSE / $SLOSH_PANE_PURPOSE. If none of them can answer, the pane stays in the tab the layout built it in -- visible, findable by purpose, and nobody else's tab -- and says so, since the fix is one variable in the caller's environment. The id-addressed verbs are probed, not assumed, and the probe is a field rather than a verb: slosh ignores arguments it does not know, so `raw` with an `id` on an older build answers ok and writes the bytes into whatever pane has focus -- silently, into somebody's shell. panes[].pid arrived with those verbs, so its presence is the capability. Where they exist: `raw id=` writes without touching focus, `capture id=` reads a pane in any tab without hopping the view (the old snapshot-and-slice route is kept as the fallback, and is rude precisely because it moves what other people are looking at), `apply-layout focus=false` builds the tab without jumping to it, and `move-pane beside=` names our pane rather than letting slosh pick that tab's focused one. Detached is now the default and `--focus` is the opt-in. The other way round, every spawn ended by pulling the session's focus -- and so the viewed tab -- to a pane nobody asked to look at: fine when a human typed the command and is waiting for it, wrong for an agent opening a binary while somebody works two tabs away. `--detached` still parses, for callers that say it. tests/test_pane_origin.py (22 checks, pure stdlib, no IDA and no slosh: the decisions are made about a `panes` reply, so the reply is the input) pins the precedence order, the focus-is-not-identity rule, the ancestry walk and that an unanswerable session leaves the pane where it was built.
-rw-r--r--idatui/pane.py201
-rw-r--r--tests/test_pane_origin.py339
2 files changed, 521 insertions, 19 deletions
diff --git a/idatui/pane.py b/idatui/pane.py
index bbc783d..851c442 100644
--- a/idatui/pane.py
+++ b/idatui/pane.py
@@ -185,6 +185,105 @@ def _kdl_str(s: str) -> str:
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
+_SLOSH_CAPS: dict[str, bool] = {}
+
+
+def _slosh_addressable() -> bool:
+ """Whether this slosh can act on a pane *by id* rather than on the focused one.
+
+ Probed, not assumed, and the probe is a field rather than a verb: slosh
+ ignores arguments it does not know, so ``raw`` with an ``id`` on an older
+ build answers ``ok`` and writes the bytes into whatever pane has focus --
+ silently, into somebody's shell. There is no way to tell from the reply, so
+ ask something that *is* visible. ``panes[].pid`` arrived with the
+ id-addressed verbs (``raw id``, ``capture``, ``move-pane beside``,
+ ``apply-layout focus``), so its presence is the capability.
+ """
+ if "addressable" not in _SLOSH_CAPS:
+ rows = _slosh_panes()
+ _SLOSH_CAPS["addressable"] = bool(rows) and "pid" in rows[0]
+ return _SLOSH_CAPS["addressable"]
+
+
+def _proc_ancestry() -> list[int]:
+ """Our own pid and every parent up to init, nearest first.
+
+ For finding which pane we are in when the environment cannot say: the pane's
+ program is one of our ancestors, and slosh reports each pane's pid.
+ """
+ out: list[int] = []
+ pid = os.getpid()
+ for _ in range(64): # a bound, not an expectation: no cycles in /proc
+ if pid <= 1:
+ break
+ out.append(pid)
+ try:
+ with open(f"/proc/{pid}/stat") as fh:
+ stat = fh.read()
+ # comm can contain spaces and brackets; everything after the last
+ # ')' is the fixed-position part, and ppid is its second field.
+ pid = int(stat[stat.rindex(")") + 2 :].split()[1])
+ except (OSError, ValueError):
+ break
+ return out
+
+
+def _slosh_origin() -> dict[str, Any] | None:
+ """The pane *we* are running in, as a `panes` row, or None if unknowable.
+
+ This is the question the whole spawn hangs on, and getting it from the
+ session's focus -- which is what this used to do -- is wrong in a way that
+ is invisible until it bites: exactly one pane in a slosh session is
+ ``focused``, it is always in the tab currently on screen, and it belongs to
+ whoever is looking. So a TUI spawned "beside the agent" landed in whatever
+ tab the human had wandered into, and in a session running several agents it
+ was reliably somebody else's. (Worse, ``apply-layout`` used to jump the view
+ to the tab it built, so one spawn moved the focus that the *next* spawn
+ would read.)
+
+ Four answers, in order, none of which is "whoever has focus":
+
+ 1. ``$IDATUI_SLOSH_PANE`` -- an operator or wrapper saying so outright.
+ 2. ``$SLOSH_PANE`` -- slosh telling the pane's program which pane it is.
+ The right answer; needs a slosh new enough to export it.
+ 3. our own pid's ancestry against ``panes[].pid`` -- the answer for a tool
+ whose environment was scrubbed, or which has no controlling tty (an
+ agent's shell-out has neither), on a slosh that reports pids.
+ 4. a purpose named by ``$IDATUI_ORIGIN_PURPOSE`` or ``$SLOSH_PANE_PURPOSE``
+ -- for an agent that labelled its own pane
+ (``printf '\\033]5577;1;purpose;agent:me'``) and exported the label, on a
+ slosh too old for either of the above. pi's slosh-status extension does
+ exactly this.
+
+ None of them means "leave the pane wherever it was built", which is a
+ predictable, harmless place. Guessing does not.
+ """
+ rows = _slosh_panes()
+ if not rows:
+ return None
+ by_id = {str(r.get("id")): r for r in rows}
+
+ for var in ("IDATUI_SLOSH_PANE", "SLOSH_PANE"):
+ want = os.environ.get(var, "").strip()
+ if want and want in by_id:
+ return by_id[want]
+
+ mine = set(_proc_ancestry())
+ for r in rows:
+ pid = r.get("pid")
+ if isinstance(pid, int) and pid > 0 and pid in mine:
+ return r
+
+ for var in ("IDATUI_ORIGIN_PURPOSE", "SLOSH_PANE_PURPOSE"):
+ purpose = os.environ.get(var, "").strip()
+ if not purpose:
+ continue
+ for r in rows:
+ if r.get("purpose") == purpose:
+ return r
+ return None
+
+
def _zellij_panes() -> list[dict[str, Any]]:
try:
out = subprocess.run(
@@ -268,7 +367,7 @@ def _pane_split(
# into a shell is the thing not to do: a pane that was *given* a command
# is the one with alive/exit_code to poll and `rerun` to repeat. So the
# pane comes from a one-tab layout and is then moved in beside us.
- origin = next((r for r in _slosh_panes() if r.get("focused")), None)
+ origin = _slosh_origin()
tag = "idatui:" + secrets.token_hex(3)
command = f"cd {_q(REPO)} && exec " + " ".join(_q(a) for a in inner)
kdl = (
@@ -276,7 +375,14 @@ def _pane_split(
f"purpose={_kdl_str(tag)} cwd={_kdl_str(REPO)} command={_kdl_str(command)}"
" } }"
)
- _slosh("apply-layout", kdl=kdl)
+ # focus=False builds the tab without moving anybody's view to it. An
+ # older slosh ignores the argument and jumps to the new tab -- rude, but
+ # that jump is also what used to *size* the pane there, so asking for it
+ # is only safe where the sizing does not depend on it.
+ if _slosh_addressable():
+ _slosh("apply-layout", kdl=kdl, focus=False)
+ else:
+ _slosh("apply-layout", kdl=kdl)
# Find it by the purpose we declared, never by "the newest id": a
# declared purpose is locked, so the program in the pane cannot rename
# itself out from under us the way a title changes.
@@ -288,17 +394,43 @@ def _pane_split(
# Into the tab we were called from, so the TUI sits beside the agent
# and is on screen for `capture`. The pane keeps its pty/scrollback,
# and the layout's now-empty tab goes away by itself.
+ #
+ # `beside` names our own pane rather than letting slosh pick that
+ # tab's focused one -- the tab's focus is a fact about the last
+ # person who looked at it -- and `focus` says whether to follow the
+ # pane there. Both are ignored by an older slosh, which lands it
+ # beside that tab's focus and takes focus with it; the tab is still
+ # right, which is the part that matters.
+ move: dict[str, Any] = {
+ "id": int(pane),
+ "tab": int(origin.get("tab_id", 0)),
+ "dir": "rows" if vertical else "cols",
+ }
+ if _slosh_addressable():
+ move["beside"] = int(origin.get("id", 0))
+ move["focus"] = not detached
try:
- _slosh(
- "move-pane",
- id=int(pane),
- tab=int(origin.get("tab_id", 0)),
- dir="rows" if vertical else "cols",
- )
+ _slosh("move-pane", **move)
except (OSError, RuntimeError):
pass # no room to split: leave it in the tab of its own
- if detached: # slosh focuses what it creates/moves; hop back
- _slosh("focus", id=int(origin.get("id", 0)))
+ if detached:
+ # Put focus back if this slosh moved it anyway, and check
+ # `panes` rather than assuming: focusing a pane also selects its
+ # tab, so a needless call here is itself a view change.
+ now = next((r for r in _slosh_panes() if r.get("focused")), None)
+ if now and str(now.get("id")) == pane:
+ _slosh("focus", id=int(origin.get("id", 0)))
+ else:
+ # We could not establish which pane we are in, so there is no "here"
+ # to put it next to. It stays in the tab the layout made -- visible,
+ # findable by purpose, and nobody else's tab. Said out loud, because
+ # the fix is a one-liner in the caller's environment.
+ print(
+ f"note: cannot tell which slosh pane this is, so {tag} stays in "
+ "its own tab (set $IDATUI_SLOSH_PANE, or use a slosh that "
+ "exports $SLOSH_PANE / reports panes[].pid)",
+ file=sys.stderr,
+ )
return pane
if mux == "zellij":
@@ -348,12 +480,18 @@ def _pane_split(
def _slosh_capture(pane: str) -> str:
- """One slosh pane's visible screen, cut out of the session snapshot.
+ """One slosh pane's visible screen.
- ``snapshot`` is the whole composited session (every visible pane, borders and
- all), and only the active tab is composited at all — so a pane parked in
- another tab is brought up and put back.
+ ``capture`` asks the pane's own terminal, so it works for a pane in any tab
+ and changes nothing. Where it is missing (an older slosh) fall back to the
+ old route: cut the pane's rect out of the session ``snapshot``, which
+ composites only the active tab and therefore has to bring the pane's tab up
+ and put it back -- visible to anybody watching, and a race against any other
+ tool doing the same. That is the fallback precisely because it is rude.
"""
+ if _slosh_addressable():
+ return _slosh("capture", id=int(pane)).get("text", "").rstrip()
+
row = _slosh_row(pane)
if row is None:
raise RuntimeError(f"no such slosh pane: {pane}")
@@ -457,11 +595,17 @@ def _pane_keys(pane: str, keys: list[str], mux: str | None = None) -> None:
mux = mux or _mux_of_pane(pane)
if mux == "slosh":
data = "".join(_to_slosh_bytes(k) for k in keys)
+ # `raw` takes the pane to write into, so no focus moves: focusing a pane
+ # also selects its tab, which moves the view of whoever is watching and
+ # loses a race with any other tool doing the same dance. `raw` rather
+ # than `send` either way: `send` is decoded as session input, where C-a
+ # is slosh's leader and would be swallowed instead of reaching the TUI.
+ if _slosh_addressable():
+ _slosh("raw", id=int(pane), data=data)
+ return
+ # An older slosh ignores `id` and writes to the focused pane, so the
+ # only safe route is the old one: focus, write, put focus back.
prev = next((r for r in _slosh_panes() if r.get("focused")), None)
- # Both `raw` and `send` land in the *focused* pane, so focus first and
- # put focus back after. `raw` rather than `send`: `send` is decoded as
- # session input, where C-a is slosh's leader and would be swallowed
- # instead of reaching the TUI.
_slosh("focus", id=int(pane))
try:
_slosh("raw", data=data)
@@ -875,7 +1019,26 @@ def main(argv: list[str]) -> int:
help="new pane size (tmux -l value, e.g. 60%% or 120; "
"ignored under slosh/zellij)",
)
- sp.add_argument("--detached", action="store_true", help="don't focus the new pane")
+ # Detached is the default, and `--focus` is the opt-in.
+ #
+ # The other way round, every spawn ended by pulling the session's focus (and
+ # so the viewed tab) to a pane nobody asked to look at -- fine when a human
+ # typed the command and is waiting for it, wrong for an agent opening a
+ # binary while somebody else works two tabs away. A caller that does want to
+ # land on it says so.
+ sp.add_argument(
+ "--focus",
+ dest="detached",
+ action="store_false",
+ default=True,
+ help="focus the new pane (default: leave focus and the viewed tab alone)",
+ )
+ sp.add_argument(
+ "--detached",
+ dest="detached",
+ action="store_true",
+ help="don't focus the new pane (the default; kept for callers that say so)",
+ )
sp.add_argument(
"--mux",
choices=MUXES,
diff --git a/tests/test_pane_origin.py b/tests/test_pane_origin.py
new file mode 100644
index 0000000..4d46e5b
--- /dev/null
+++ b/tests/test_pane_origin.py
@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+"""Which pane a spawn thinks it is next to (pure stdlib, no IDA, no slosh).
+
+The bug being pinned: the slosh backend used to work out "where do I live" from
+`panes[].focused`. Exactly one pane in a slosh session is focused, it is always
+in the tab currently on screen, and it belongs to whoever is looking -- so a
+TUI spawned "beside the agent" landed in whatever tab the human had wandered
+into, and in a session running several agents it was reliably a stranger's.
+
+The rule now: identity comes from the environment ($SLOSH_PANE), or from our own
+process ancestry against `panes[].pid`, or from a purpose we were told to look
+for -- and if none of those can answer, the pane stays in the tab it was built
+in rather than being placed on a guess.
+
+slosh is faked here on purpose: these are decisions the backend makes about a
+`panes` reply, so the reply is the input. tests/test_own_pane.py in the slosh
+repo covers the other side (that slosh really does export $SLOSH_PANE, report
+pids, and honour `beside`/`focus:false`).
+"""
+
+#: origin resolution + spawn placement, pure stdlib.
+#: Read by tests/run.py (--fast skips every NEEDS_IDA file).
+NEEDS_IDA = False
+import os
+import sys
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from idatui import pane as P # noqa: E402
+
+PASS = FAIL = 0
+
+
+def check(name, cond, detail=""):
+ global PASS, FAIL
+ if cond:
+ PASS += 1
+ print(f"ok {name}")
+ else:
+ FAIL += 1
+ print(f"FAIL {name} <- {detail}")
+
+
+# The session every test below reasons about: the agent is pane 1 in tab 1, the
+# human is pane 2 in tab 2 and holds the session's only focus.
+AGENT = {
+ "id": 1,
+ "tab_id": 1,
+ "pid": 4242,
+ "focused": False,
+ "purpose": "",
+ "alive": True,
+ "w": 80,
+ "h": 24,
+}
+HUMAN = {
+ "id": 2,
+ "tab_id": 2,
+ "pid": 5252,
+ "focused": True,
+ "purpose": "",
+ "alive": True,
+ "w": 80,
+ "h": 24,
+}
+
+
+class FakeSlosh:
+ """Records every call, and answers `panes` from a list we control."""
+
+ def __init__(self, rows, spawned_id=9):
+ self.rows = [dict(r) for r in rows]
+ self.calls = []
+ self.spawned_id = spawned_id
+
+ def __call__(self, cmd, **params):
+ self.calls.append((cmd, params))
+ if cmd == "panes":
+ return {"ok": True, "panes": self.rows}
+ if cmd == "apply-layout":
+ # slosh builds the pane in a tab of its own; give it back with the
+ # purpose the caller declared, which is how the backend finds it.
+ kdl = params.get("kdl", "")
+ purpose = kdl.split('purpose="', 1)[-1].split('"', 1)[0]
+ self.rows.append(
+ {
+ "id": self.spawned_id,
+ "tab_id": 99,
+ "pid": 6262,
+ "focused": False,
+ "purpose": purpose,
+ "alive": True,
+ "w": 80,
+ "h": 24,
+ }
+ )
+ return {"ok": True}
+ if cmd == "move-pane":
+ for r in self.rows:
+ if r["id"] == params.get("id"):
+ r["tab_id"] = params.get("tab")
+ return {"ok": True, "tab": params.get("tab")}
+ return {"ok": True}
+
+ def of(self, cmd):
+ return [p for c, p in self.calls if c == cmd]
+
+
+def with_fake(rows, env=None, ancestry=(), spawned_id=9):
+ """Install a fake slosh + environment; returns (fake, restore callable)."""
+ fake = FakeSlosh(rows, spawned_id=spawned_id)
+ saved = {
+ "slosh": P._slosh,
+ "ancestry": P._proc_ancestry,
+ "caps": dict(P._SLOSH_CAPS),
+ "env": {
+ k: os.environ.get(k)
+ for k in (
+ "SLOSH_PANE",
+ "IDATUI_SLOSH_PANE",
+ "IDATUI_ORIGIN_PURPOSE",
+ "SLOSH_PANE_PURPOSE",
+ )
+ },
+ }
+ P._slosh = fake
+ P._proc_ancestry = lambda: list(ancestry)
+ P._SLOSH_CAPS.clear()
+ for k in saved["env"]:
+ os.environ.pop(k, None)
+ for k, v in (env or {}).items():
+ os.environ[k] = v
+
+ def restore():
+ P._slosh = saved["slosh"]
+ P._proc_ancestry = saved["ancestry"]
+ P._SLOSH_CAPS.clear()
+ P._SLOSH_CAPS.update(saved["caps"])
+ for k, v in saved["env"].items():
+ os.environ.pop(k, None)
+ if v is not None:
+ os.environ[k] = v
+
+ return fake, restore
+
+
+def origin_with(**kw):
+ fake, restore = with_fake(**kw)
+ try:
+ return P._slosh_origin()
+ finally:
+ restore()
+
+
+def test_origin_is_never_the_focused_pane():
+ o = origin_with(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "1"})
+ check(
+ "$SLOSH_PANE wins over the focused pane",
+ o and o["id"] == 1,
+ f"got {o}",
+ )
+
+ o = origin_with(rows=[AGENT, HUMAN], ancestry=(1111, 4242, 1))
+ check(
+ "our own pid's ancestry finds our pane",
+ o and o["id"] == 1,
+ f"got {o}",
+ )
+
+ o = origin_with(
+ rows=[AGENT, HUMAN],
+ env={"IDATUI_ORIGIN_PURPOSE": "agent:me"},
+ )
+ check("an unset purpose does not match an empty one", o is None, f"got {o}")
+
+ tagged = dict(AGENT, purpose="agent:me")
+ o = origin_with(rows=[tagged, HUMAN], env={"IDATUI_ORIGIN_PURPOSE": "agent:me"})
+ check("a declared purpose is the last resort", o and o["id"] == 1, f"got {o}")
+
+ # What pi's slosh-status extension publishes for its own pane.
+ tagged = dict(AGENT, purpose="agent:pi.1234")
+ o = origin_with(rows=[tagged, HUMAN], env={"SLOSH_PANE_PURPOSE": "agent:pi.1234"})
+ check("$SLOSH_PANE_PURPOSE works too", o and o["id"] == 1, f"got {o}")
+
+ o = origin_with(rows=[AGENT, HUMAN])
+ check(
+ "with nothing to go on the answer is None, never the focused pane",
+ o is None,
+ f"got {o} (the focused pane is the human's)",
+ )
+
+
+def test_an_override_beats_everything():
+ o = origin_with(
+ rows=[AGENT, HUMAN],
+ env={"IDATUI_SLOSH_PANE": "2", "SLOSH_PANE": "1"},
+ ancestry=(4242,),
+ )
+ check("$IDATUI_SLOSH_PANE overrides $SLOSH_PANE", o and o["id"] == 2, f"got {o}")
+
+ o = origin_with(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "777"}, ancestry=(4242,))
+ check(
+ "an id that is not in the session falls through to the pid",
+ o and o["id"] == 1,
+ f"got {o}",
+ )
+
+
+def test_a_spawn_lands_beside_us_and_takes_no_focus():
+ fake, restore = with_fake(rows=[AGENT, HUMAN], env={"SLOSH_PANE": "1"})
+ try:
+ got = P._pane_split(
+ ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True
+ )
+ finally:
+ restore()
+
+ check("the spawn returns the new pane's id", got == "9", f"got {got!r}")
+
+ layouts = fake.of("apply-layout")
+ check(
+ "the layout is applied without taking the view",
+ layouts and layouts[0].get("focus") is False,
+ f"{layouts}",
+ )
+
+ moves = fake.of("move-pane")
+ check("the pane is moved once", len(moves) == 1, f"{moves}")
+ if moves:
+ check(
+ "into the tab of the pane we are in, not the focused tab",
+ moves[0].get("tab") == AGENT["tab_id"],
+ f"went to tab {moves[0].get('tab')}, human is in {HUMAN['tab_id']}",
+ )
+ check(
+ "beside our own pane, not that tab's focus",
+ moves[0].get("beside") == AGENT["id"],
+ f"{moves[0]}",
+ )
+ check(
+ "and without taking focus",
+ moves[0].get("focus") is False,
+ f"{moves[0]}",
+ )
+ check(
+ "nothing is focused on the way out",
+ not fake.of("focus"),
+ f"{fake.of('focus')}",
+ )
+
+
+def test_focus_is_restored_only_when_slosh_moved_it():
+ """`--detached` on an older slosh: it ignores `focus`, so we put it back."""
+ rows = [AGENT, HUMAN]
+ fake, restore = with_fake(rows=rows, env={"SLOSH_PANE": "1"})
+ # An older slosh: no `pid` in the rows, so the backend must not pass the
+ # newer arguments -- and must fix up focus itself.
+ old = [{k: v for k, v in r.items() if k != "pid"} for r in rows]
+ fake.rows = old
+ P._slosh = fake
+ P._SLOSH_CAPS.clear()
+ try:
+ P._pane_split(
+ ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True
+ )
+ moves = fake.of("move-pane")
+ check(
+ "an older slosh is not sent arguments it would ignore",
+ moves and "beside" not in moves[0] and "focus" not in moves[0],
+ f"{moves}",
+ )
+ check(
+ "the layout is applied the old way too",
+ fake.of("apply-layout") and "focus" not in fake.of("apply-layout")[0],
+ f"{fake.of('apply-layout')}",
+ )
+ check(
+ "focus is not put back when it never moved",
+ not fake.of("focus"),
+ f"{fake.of('focus')}",
+ )
+ finally:
+ restore()
+
+ # Now the same, with the new pane holding focus afterwards: put it back.
+ fake2, restore2 = with_fake(rows=rows, env={"SLOSH_PANE": "1"})
+ fake2.rows = [{k: v for k, v in r.items() if k != "pid"} for r in rows]
+ P._slosh = fake2
+ P._SLOSH_CAPS.clear()
+ orig_call = fake2.__call__
+
+ def stealing(cmd, **params):
+ out = orig_call(cmd, **params)
+ if cmd == "move-pane": # slosh focuses what it moves
+ for r in fake2.rows:
+ r["focused"] = r["id"] == params.get("id")
+ return out
+
+ P._slosh = stealing
+ try:
+ P._pane_split(
+ ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True
+ )
+ check(
+ "focus is put back when slosh did move it",
+ [p.get("id") for p in fake2.of("focus")] == [AGENT["id"]],
+ f"{fake2.of('focus')}",
+ )
+ finally:
+ restore2()
+
+
+def test_without_an_identity_it_stays_put():
+ fake, restore = with_fake(rows=[AGENT, HUMAN]) # nothing to go on
+ try:
+ got = P._pane_split(
+ ["/bin/true"], mux="slosh", vertical=False, size=None, detached=True
+ )
+ finally:
+ restore()
+ check("the pane is still created", got == "9", f"got {got!r}")
+ check(
+ "but never moved into somebody else's tab",
+ not fake.of("move-pane"),
+ f"{fake.of('move-pane')}",
+ )
+ check("and focus is left alone", not fake.of("focus"), f"{fake.of('focus')}")
+
+
+def main():
+ for name, fn in sorted(globals().items()):
+ if name.startswith("test_"):
+ fn()
+ print(f"\n{PASS} passed, {FAIL} failed")
+ return 1 if FAIL else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())