<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/tests, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/tests?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/tests?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-09-29T12:31:26Z</updated>
<entry>
<title>Put a spawned pane beside US, not beside whoever is looking</title>
<updated>2026-09-29T12:31:26Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-29T12:31:26Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6252b7c6a89043cfee636aa418303c5a86b4d108'/>
<id>urn:sha1:6252b7c6a89043cfee636aa418303c5a86b4d108</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>Spawn TUI panes in slosh, the multiplexer we actually use</title>
<updated>2026-09-29T11:55:30Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-29T11:55:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=432a8e5f57b65d38a2a377a54994dd6b73216742'/>
<id>urn:sha1:432a8e5f57b65d38a2a377a54994dd6b73216742</id>
<content type='text'>
`pane spawn` knew tmux and zellij, so under slosh every entry point
answered "error: not inside tmux or zellij" — the agent skill described a
workflow that could not run. slosh is now the first thing autodetected
($SLOSH, then $ZELLIJ, then $TMUX), and a slosh pane id is a bare integer,
so a registry row written before this commit still routes its own
stop/capture/keys.

Everything goes through the control socket, always the JSON form
($SLOSH_BIN -s $SLOSH_SESSION cmd): the bare-verb alias answers unwrapped,
and a parser written for one shape silently finds nothing in the other.
$SLOSH_BIN rather than `slosh` because a session may have been started
from a build tree that is not on $PATH.

Spawning is the one place slosh differs in kind. Its `split` verb only
ever makes a shell pane, and typing a command into somebody's 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 spawn applies a one-tab
layout whose pane carries command= and a declared purpose=idatui:&lt;hex&gt;,
finds it by that purpose (declared purposes are locked, so the TUI cannot
relabel itself out from under us, and titles change anyway), then
move-panes it into the tab we were called from: it lands beside the
caller with its pty and scrollback intact, and the layout's now-empty tab
removes itself. --detached restores focus afterwards, since slosh focuses
what it creates and moves.

The rest follows the zellij precedent. A dead command pane stays on
screen as a husk, which _pane_alive must read as dead or a launcher that
died on startup would burn the whole 300s readiness timeout instead of
reporting "pane exited during startup". `capture` cuts the pane's
content_* rect out of `snapshot` (which is the whole composited session)
and hops tabs if the pane is parked in another one, because only the
active tab is composed at all. `keys` keeps taking tmux-flavoured names
and turns them into the bytes a terminal sends: it focuses the target,
writes with `raw`, and restores focus — `send` would be decoded as
session input first, where C-a is slosh's own leader and never reaches
the TUI. --size stays tmux-only and now says so for whichever mux is in
play.

The two RPC integration tests gated on $TMUX/$ZELLIJ, so they skipped
silently under slosh; they accept $SLOSH now.

Verified against the live session, not just in unit checks: a probe pane
spawns, reports alive, captures its own output, receives real keystrokes
and dies on close; `pane spawn --open targets/echo` reaches ready:true
with 128 functions, `drive where` answers, `pane keys Tab` flips listing
to pseudocode on screen, and `pane stop` leaves an empty registry and no
husk. Env inheritance is unchanged from zellij (a pane gets the session
server's environment, not the caller's) — the probe printed PROBE_VAR=[].
</content>
</entry>
<entry>
<title>Offer a writable database location instead of failing the open</title>
<updated>2026-09-25T12:59:48Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-25T12:59:48Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=15425df0a6e09984fdab3f86354620d4d8656806'/>
<id>urn:sha1:15425df0a6e09984fdab3f86354620d4d8656806</id>
<content type='text'>
`ida-tui /bin/ls` asked IDA to create /bin/ls.i64, which a normal user
cannot write, and what came back was the backend's account of it:

    idalib worker launcher 2425195 exited with status 1
    [ida-nexus] Failed to open database /usr/bin/ls

after a full analysis wait, naming neither the file it could not create
nor the reason. Nothing in the message points at permissions, and there
was no way forward from it.

idatui/idbpath.py answers the question before anything is opened: where
would the database go (mirroring ida_nexus's own derivation), can it go
there (os.access only — the pre-flight check must not create files), and
where instead. The proposal is stable per binary
(~/.local/share/idatui/db/&lt;name&gt;-&lt;sha1(realpath)[:8]&gt;.i64) so reopening
the same binary resumes the same names and comments, and hashed so two
busyboxes from different directories cannot collide. DATA_HOME rather
than CACHE_HOME on purpose: an .i64 holds the whole session, and cache
directories are something cleaners may empty.

The answer becomes NexusClient(output_database=...) — the option project
mode already uses to keep everything in its sidecar. `--idb PATH` (also
forwarded by `pane spawn`) says where without being asked. An RPC-driven
TUI is never asked: `pane spawn` polls for ready and would sit out its
whole timeout in front of a modal nobody can answer, so it relocates and
says so in the status line. A failed open re-offers relocation after a
real create probe, since os.access does not know about ACLs, a lying
network mount or a full disk.

tests/test_idbpath.py (pure, 32 checks) pins the path arithmetic, the two
properties of the proposal and that neither check touches the user's
directory; tests/test_idb_relocate_ui.py (textual, no worker, 24 checks)
drives the dialog, Esc, an edited path, the untouched common case, --idb,
the RPC path, and that output_database actually reaches the client.
Verified end to end against a real /bin/ls: 400 functions, and a rename
saved in one session comes back in the next.
</content>
</entry>
<entry>
<title>Fix parsing of pseudocode tags</title>
<updated>2026-09-25T11:53:16Z</updated>
<author>
<name>Duncan Ogilvie</name>
<email>mr.exodia.tpodt@gmail.com</email>
</author>
<published>2026-09-25T11:53:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=65b57fdd2ebbb4884fc13a81061af60c613cfd16'/>
<id>urn:sha1:65b57fdd2ebbb4884fc13a81061af60c613cfd16</id>
<content type='text'>
</content>
</entry>
<entry>
<title>scenarios: cover the event-&gt;UI auto-refresh path</title>
<updated>2026-08-21T10:20:02Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:20:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=820c356204cb5552a2b4b4ad3591a134d1b6d1b0'/>
<id>urn:sha1:820c356204cb5552a2b4b4ad3591a134d1b6d1b0</id>
<content type='text'>
PR #1's live refresh had client-level coverage only (the listener thread
and debounce, in test_nexus_client); the app half -- _refresh_idb_events
invalidating, reindexing, and reloading the active surface -- was untested.

The scenario renames straight through the client so no UI cache hears
about it (what another client's edit looks like from this process), hands
the app a synthetic batch, and requires: the status says why the view is
about to move, the listing rebuilds around the same cursor and viewport
address, the function index carries the external name, pseudocode
decompiled afterwards shows it, and a second batch landing while decomp is
active reloads it in place. That second batch also reverts the rename, so
the scenario is idempotent.
</content>
</entry>
<entry>
<title>reformat: ruff format + import sort, mechanically (see ruff.toml)</title>
<updated>2026-08-21T10:15:15Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:14:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=02d02417800184fb76cd0245cdaa94c437aa4081'/>
<id>urn:sha1:02d02417800184fb76cd0245cdaa94c437aa4081</id>
<content type='text'>
No behavior. Listed in .git-blame-ignore-revs (next commit).
</content>
</entry>
<entry>
<title>tests: repair the two seams the ida-nexus port left behind</title>
<updated>2026-08-21T10:12:53Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:12:53Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=ea22067c52ec335ea4d3a616cb2107d5fdbdf7b6'/>
<id>urn:sha1:ea22067c52ec335ea4d3a616cb2107d5fdbdf7b6</id>
<content type='text'>
Two scenario monkeypatch sites still hooked client.invoke -- the rename to
call() updated the call sites but not the patches, so reprime_is_free and
split_view crashed instead of counting. call() takes the remote_ops
declaration itself now, so both count by its __name__.

remote_ops imported RemoteModule inside _bindings(), which made
test_nexus_client (NEEDS_IDA = False) unrunnable under a stdlib-only
python3 -- the house rule tests/run.py --fast depends on. The import moves
to the same eagerly-if-present, bound-to-None-so-patchable contract
nexus_client uses, and the test injects FakeRemoteModule/FakeRemoteError
exactly like its other fakes: real names when the library is installed,
strict fakes when it is not.
</content>
</entry>
<entry>
<title>Merge PR #1 from mrexodia: Windows support and auto-refresh on events</title>
<updated>2026-08-21T10:09:21Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:09:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=9541043c9dee7973c140159b3af561c6d0405e9e'/>
<id>urn:sha1:9541043c9dee7973c140159b3af561c6d0405e9e</id>
<content type='text'>
https://github.com/blasty/ida-tui/pull/1 — from the ida-nexus (né
ida-codemode) maintainer: kitty-graphics fallback for platforms without
termios, Ctrl+R view refresh, typed remote operations through ida_nexus
RemoteModule, live auto-refresh from other clients' IDB events (upstream
item 7), discard-without-save (item 6), and the ida-nexus 0.7.0 rename.

tests/test_kittygfx.py is the union of both sides' files: our escape-
construction checks plus the PR's cross-platform fallback checks.
</content>
</entry>
<entry>
<title>splash: give the logo a placement id so re-anchoring replaces, not stacks</title>
<updated>2026-08-21T10:08:42Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:08:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=7a3b198a904da186cc2860df059b063db24282ea'/>
<id>urn:sha1:7a3b198a904da186cc2860df059b063db24282ea</id>
<content type='text'>
A kitty placement is identified by (image id, placement id); an a=p with no
p= key is anonymous and every one stacks another copy. The splash re-anchors
on every progress note (~5/s), so a 60s load ended with ~300 placements of an
RGBA logo alpha-compositing over each other -- soft edges creeping to solid,
and the terminal re-rendering all of them per frame.

Same pair every time (LOGO_PLACEMENT) = the terminal REPLACES the placement,
so re-anchoring is free and atomic. That is also why on_resize no longer
clear()s first (that showed a hole for a frame), and why a shrunk-to-nothing
region now drops the placement instead of leaving a stale one anchored.

tests/test_kittygfx.py pins the escapes (pure, stdlib);
experiments/splash_place_count.py counts what a real splash sends.
</content>
</entry>
<entry>
<title>Switch to ida-nexus</title>
<updated>2026-08-20T21:55:52Z</updated>
<author>
<name>Duncan Ogilvie</name>
<email>mr.exodia.tpodt@gmail.com</email>
</author>
<published>2026-08-20T21:55:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f4c1d9b5497fd0d38137b6b345e8171b307c1117'/>
<id>urn:sha1:f4c1d9b5497fd0d38137b6b345e8171b307c1117</id>
<content type='text'>
</content>
</entry>
</feed>
