<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-09-29T12:31:42Z</updated>
<entry>
<title>TODO.new: two bugs found while driving the TUI (symbol-list pageup/down, multiline comments)</title>
<updated>2026-09-29T12:31:42Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-29T12:31:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=3a1403b95a80460e45e81dc6e94eca697372781d'/>
<id>urn:sha1:3a1403b95a80460e45e81dc6e94eca697372781d</id>
<content type='text'>
</content>
</entry>
<entry>
<title>experiments: two pty harnesses the pilot cannot stand in for</title>
<updated>2026-09-29T12:31:42Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-29T12:31:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6f05712453c303d71cf9b36bfb07732eb868500c'/>
<id>urn:sha1:6f05712453c303d71cf9b36bfb07732eb868500c</id>
<content type='text'>
Both exist because an in-process Textual pilot passes on things a real
terminal does not do.

type_bench.py times the semantic verbs at several delay_ms values through
a real pty -- tests/_fixtures.fast_keys patches the per-key idle wait
away, which is exactly the cost being measured, so the pilot is blind to
it. This is where the ~80ms/char figure behind `press()` came from.

click_pty.py injects SGR mouse bytes on stdin like a terminal emulator
and reads the cursor back over the RPC. `pilot.click()` posts
MouseDown/MouseUp/Click straight at a widget and bypasses Textual's own
click synthesis, so it can pass while a real click does nothing.
</content>
</entry>
<entry>
<title>Type at the speed the driver asked for</title>
<updated>2026-09-29T12:31:36Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-29T12:31:36Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=29c529796b612b3d390a7f8ed6f83269fd88c67e'/>
<id>urn:sha1:29c529796b612b3d390a7f8ed6f83269fd88c67e</id>
<content type='text'>
`App._press_keys` waits on Textual's `wait_for_idle` twice per key -- a
CPU-load heuristic that sleeps in 20ms granules until process time stops
advancing. Measured through the RPC server on a real pty that is ~80ms
per character, so `delay_ms=35` typed at ~115ms/char (a third of the pace
it named) and `delay_ms=0` -- what `idatui.drive` asks for, and what an
agent wants -- was no faster than a slow human.

`press()` swaps the heuristic for a bare yield while our keys go in, the
same trade tests/_fixtures.fast_keys already makes. What replaces it is
not "nothing": every verb ends in settle() (pump drained, threaded
workers finished), which is the real gate and strictly stronger than "the
CPU looks idle", and `wait:&lt;ms&gt;` tokens still sleep for real so a delay
that was asked for still renders frame by frame. It patches around the
call rather than at import, so nothing else -- the pilot suite in
particular -- changes behaviour if this module is imported there.

With the floor gone, the default delay is the whole cost of a keystroke,
which makes 35 dishonest as well as slow: TYPE_DELAY_MS is 20 (~50
chars/second, still visibly typed). docs/RPC.md says what delay_ms now
means, since "0 is instant" only became true here.

Also moves the last two `client.invoke("name", ...)` call sites in this
file to `client.call(remote_ops.name, ...)`, which is what the rest of
the tree uses since the ida-nexus port: the operation is a reference, not
a string nobody checks.
</content>
</entry>
<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>Merge PR #2 from mrexodia: fix pseudocode/listing tag parsing, ida-nexus 0.13.0</title>
<updated>2026-09-25T12:28:45Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-25T12:28:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e'/>
<id>urn:sha1:9dea7bc83ff9b3b952a761a2d0f92b75abdd6e7e</id>
<content type='text'>
https://github.com/blasty/ida-tui/pull/2 — pseudocode address anchors are
now split off only as the trailing /*0x…*/ our decompile tool appends, so a
marker-shaped string literal or source comment survives display and cursor
columns. The listing span walker asks the kernel (tag_skipcode) how long
each colour code is instead of assuming two bytes, so semantic group tags
with kind/TID headers stay invisible on kernels that emit them. Bumps
ida-nexus to 0.13.0.

Verified locally on IDA 9.4 + ida-nexus 0.13.0: full suite 974 passed
(955 on main + 19 new pure checks). Old vs new span walker give identical
output over 60k bash heads (no 0x36 tags on this kernel); the new walker
costs 19.8 vs 12.3 us/line, next to 7.6 us for generate_disasm_line.
</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>Bump ida-nexus to 0.13.0</title>
<updated>2026-09-25T11:52:02Z</updated>
<author>
<name>Duncan Ogilvie</name>
<email>mr.exodia.tpodt@gmail.com</email>
</author>
<published>2026-09-25T11:52:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6f98a1d3eabbeb991121bf837e1df4f618667c05'/>
<id>urn:sha1:6f98a1d3eabbeb991121bf837e1df4f618667c05</id>
<content type='text'>
</content>
</entry>
<entry>
<title>README: friendlier rewrite; move IDA connection internals to CONTRIBUTING</title>
<updated>2026-09-25T09:56:54Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-09-25T09:56:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=220c6144095d9c6dae0a2cd993b51c18bd7f5953'/>
<id>urn:sha1:220c6144095d9c6dae0a2cd993b51c18bd7f5953</id>
<content type='text'>
Plain-language intro, numbered getting-started steps, readable keys and
feature tables, advanced usage folded into a details block. The lease /
event-stream / remote-op details now live under 'How ida-tui talks to IDA'
in CONTRIBUTING.md so nothing is lost.
</content>
</entry>
</feed>
