<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/rpc.py, branch pre-codemode</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/rpc.py?h=pre-codemode</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/rpc.py?h=pre-codemode'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-06T22:33:28Z</updated>
<entry>
<title>diag: somewhere for swallowed errors to go</title>
<updated>2026-08-06T22:33:28Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T22:33:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=2b0ae8df463edb597007186688ee557c18f1ac18'/>
<id>urn:sha1:2b0ae8df463edb597007186688ee557c18f1ac18</id>
<content type='text'>
A TUI must not die because one background load failed, so this codebase catches
broadly -- ~50 `except Exception` sites, two dozen resolving to `pass`. Right
policy, one bad consequence: with 44 `@work(thread=True)` workers, a failure in
a background load leaves no trace whatsoever. The view stays empty and there is
nothing to read afterwards, because the app owns the screen.

kittygfx already solved this for itself with $IDATUI_KITTY_LOG. idatui/diag.py
is the same idea for everything else: $IDATUI_LOG writes every swallowed error
plus its traceback to a file, and the last 50 are kept in memory regardless so a
driver can ask a live app what went wrong. Unset, it costs an environ lookup.

Wired in where losing the error changes a DECISION rather than just a pixel:

  * rename: a resolve() that throws renames as DATA instead of as a function.
  * name: a function_of() that throws means we never learn the address is a
    function start, so the index keeps the old name and every readback says the
    rename didn't happen.
  * retype: a resolve() that throws retypes the ENCLOSING function instead.
  * decompile: a failed full-body fetch silently returns CLIPPED pseudocode.
  * trail: a failed decomp_map stops the pseudocode being painted, silently.

Deliberately NOT wired into the query_one guards -- a modal owning the screen is
normal and constant, and logging it would bury the real entries in noise.

New RPC verb `diag {n?, clear?}`, documented in docs/RPC.md: the answer to "the
verb reported success and the pane shows nothing".

Also a flake, same shape as the others: follow_xrefs waited on the nav depth but
asserted on _cur, and a follow pushes the source entry BEFORE opening the
target -- so the check could run in between and see the function it jumped
from. About one run in ten. It waits on the postcondition it asserts now; three
clean full runs since.

833 checks; --fast is 344 in 3.5s.
</content>
</entry>
<entry>
<title>app: the view mode is a type, and 'disasm' is gone</title>
<updated>2026-08-06T22:07:22Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T22:07:22Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=cefdeb88ab3271313db741c9c95677178b18d484'/>
<id>urn:sha1:cefdeb88ab3271313db741c9c95677178b18d484</id>
<content type='text'>
_active was a bare string with 49 comparisons across four modules and a fifth
value nobody meant to keep. "disasm" was assigned on exactly one path -- a
decompile that failed with nowhere to return to -- and named the same widget as
"listing". Four sites understood it; five compared against "listing" alone and
silently took the wrong branch:

  * Tab out of a failed decompile set "listing" instead of "decomp", so the
    first press appeared to do nothing.
  * rpc.py carried a workaround for a mode change that never arrived, keyed on
    being ALREADY in the ghost state -- so it fired in the rare case and not in
    the common one. Now keyed on LISTING, which is the case that happens.
  * drive.py asked the socket to show it "disasm", a value the app will now
    never report, and would have toggled twice and given up.

ViewMode is a StrEnum on purpose: _active goes straight to drivers as
cursor.kind and the pilot compares it to plain strings, so members being strings
keeps every payload and comparison working. What it buys is one place that says
which modes exist, and an AttributeError instead of silence on a typo.

Read it through is_listing/is_decomp/is_hex/is_graph/in_code rather than ==.
The bare comparisons are what let the ghost hide, and they are what the next
mode would have to hunt down -- adding "graph" already cost one crash that way
(_active_code_view returning None when a prompt closed).

view_modes_all_handled walks the enum and asks the app the questions it asks
itself. Verified it bites: adding a fifth unhandled member fails it twice.

746 checks, 142.3s.
</content>
</entry>
<entry>
<title>app: lift trace navigation out of IdaTui</title>
<updated>2026-08-06T20:12:56Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T20:12:56Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6f9cf89c0804eba19fb2d87469d2d2ab28f8049b'/>
<id>urn:sha1:6f9cf89c0804eba19fb2d87469d2d2ab28f8049b</id>
<content type='text'>
First cut at the 4000-line class. Trace is the cleanest seam: 348 contiguous
lines, one coherent job (where we are in time and everything that moves us),
and two suites already covering it.

TraceController owns the state now -- the trace, the timestamp, the trail maps.
IdaTui keeps the keys, because Textual only merges BINDINGS from DOMNode
subclasses and a mixin's would be silently dropped, and it keeps the @work entry
points, because the worker machinery wants a DOMNode host. Both are one-line
delegates.

_trace/_t/_trail_map/_trail_map_ea/_trail_line_of stay readable on the app as
properties: the pilot suite and rpc.py read the position by those names, and a
property means one owner rather than a copy that can drift. rpc.py itself now
goes through the controller.

The parallel line-map that _apply_split_map used to poke into five attributes is
now one adopt_map() call -- same single shared index, but the sharing is stated
rather than implied by two places assigning the same fields.

731 checks, unchanged.
</content>
</entry>
<entry>
<title>rpc: a graph verb</title>
<updated>2026-08-06T13:11:54Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T13:11:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=25457de103f8929b76557afcae88edb4162ee9bc'/>
<id>urn:sha1:25457de103f8929b76557afcae88edb4162ee9bc</id>
<content type='text'>
It reports STRUCTURE -- blocks, typed edges, ranks, box geometry, the
cursor -- and not the box-drawing characters, which is what a driver
actually wants; screen still gives you the drawing. show is a pure read.
The line-oriented verbs refuse in the graph rather than reporting a
(block, row) cursor as a line index some later edit would trust.
</content>
</entry>
<entry>
<title>rpc: an opfmt verb, and 'drive fmt'</title>
<updated>2026-08-06T13:11:54Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T13:11:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=fcfdc8c941aeb87cc99c983146f553e03a5daa49'/>
<id>urn:sha1:fcfdc8c941aeb87cc99c983146f553e03a5daa49</id>
<content type='text'>
mode is cycle/back/show or an explicit format. 'show' reports the current
format and the stops on offer without editing, which is what a driver
needs: the rendered text alone can't be trusted (a listing read before an
ARM/Thumb switch shows the old decoding). 'word' puts the cursor on a
token first, so a literal can be named instead of steered to.
</content>
</entry>
<entry>
<title>rpc: rename_many drops the Hex-Rays cache too</title>
<updated>2026-08-01T15:22:51Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-01T15:22:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=384846df7469b1dcc1842b6bdd6ccb0732071058'/>
<id>urn:sha1:384846df7469b1dcc1842b6bdd6ccb0732071058</id>
<content type='text'>
Hex-Rays caches per function and does not notice that a *callee* was renamed;
worse, that cache is persisted in the .i64, so a bulk import left pseudocode
calling sub_98C0 forever while the listing and every readback said memset --
the exact readback disagreement a driver cannot detect. Batch now calls
force_recompile before bumping the local caches.

Test extended: decompile, rename via rename_many, read the pseudocode back.
</content>
</entry>
<entry>
<title>rpc: make a raw firmware image drivable (load options, define, bulk symbols)</title>
<updated>2026-08-01T15:18:57Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-01T15:18:57Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=14ca7c7e5c56c3fdd059d168957a2b2e3dbe504e'/>
<id>urn:sha1:14ca7c7e5c56c3fdd059d168957a2b2e3dbe504e</id>
<content type='text'>
Opening a headerless blob was the one workflow that fell out of the driving
surface entirely, and each gap hid the next:

- `pane spawn` couldn't pass --processor/--base/--ida-args, so the pane came up
  "ready" with zero functions (x86 at 0) and the only way through was to
  hand-write a project file. It now forwards them to idatui.launch.
- c/p/t/T (code, function, ARM&lt;-&gt;Thumb, vector scan) existed as listing
  bindings with no verb, so a driver had to guess raw keys -- and raw keys are
  swallowed by whatever modal happens to be up. `define {kind,target?}` goes
  through the app's own edit worker and reports what IDA actually did.
- every name went through the typed rename prompt: a navigation (listing page +
  decompile) plus two prompt round-trips each. A 427-symbol map took tens of
  minutes of driving. `rename_many {items|file}` hands IDA's rename tool the
  whole list in one call (371 symbols in 3s) and refreshes the caches and the
  function table once.

drive gains `define &lt;kind&gt; [target...]` and `syms &lt;file.json&gt;`.

Verified live against a real pane (tests/test_rawimage_rpc.py, 13 checks:
spawn load options, define thumb/func + unknown-kind rejection, rename_many
from a file and inline, with resolve/functions readback).
</content>
</entry>
<entry>
<title>rpc: comments are instant and newlines no longer vanish</title>
<updated>2026-08-01T13:50:11Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-01T13:50:11Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6d87b75e7686875598ba49f4223d7e2be5a9e241'/>
<id>urn:sha1:6d87b75e7686875598ba49f4223d7e2be5a9e241</id>
<content type='text'>
Two fixes to the comment verb:

Drop the per-character typing delay. Rename/retype/goto use a 35 ms delay for
the visual effect (the agent's keystrokes appear one by one on the livestream),
but comments can be long — a 200-char annotation blocked the driver for 7 s of
pure animation. Comments now type instantly (delay=0); the aesthetic delay is
kept for rename, retype and goto where values are short.

Escape literal newlines before injecting into the prompt. The Input widget is
single-line, so a real 0x0a sent as a keystroke was silently swallowed. The
app's _do_comment already converts the two-char sequence '\\n' into a real
newline for IDA, so the RPC layer now does text.replace('\\n', '\\\\n')
before typing — both literal newlines from the caller and explicit \\n in the
text reach IDA as multi-line comments (each line gets its own // prefix in the
decompiler).

Verified on a live pane: a comment with an embedded newline now renders as two
// lines in the pseudocode, and long comments appear without the multi-second
typing pause.

tests/test_scenarios.py: 212 passed, 0 failed.
</content>
</entry>
<entry>
<title>rpc: a navigation that timed out reported success and corrupted the next edit</title>
<updated>2026-07-31T07:47:15Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-31T07:47:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=0635d85d23ec04a130bcacb2f25cd74d680f4ef7'/>
<id>urn:sha1:0635d85d23ec04a130bcacb2f25cd74d680f4ef7</id>
<content type='text'>
goto/open ran `settle(app, pred, timeout)` and threw the result away. On a large
database the listing build routinely outruns the default 20 s, so the verb
returned a normal snapshot while the view had not moved. Every subsequent
rename/comment then applied to wherever the caller *used* to be.

Reproduced on a live pane against a 4 MB Go binary: `goto 0x1002019b0`
returned ok with the view still at 0x100001000, and the following
`rename main_inflate_zlib` renamed internal/abi.BoundsDecode instead — then the
rename's own snapshot showed `main_inflate @ 0x1002019b0`, because by the time
it was taken the goto had finally landed. Success reported, right-looking
readback, wrong function edited, and it survived a save.

This is what made an agent session stamp net_writeFull onto main_usage and
conclude the tooling was flaky.

goto/open now raise TimeoutError naming the target and where we actually are,
suggesting a larger timeout=. _press() does the same for follow/toggle_view/
hex/xrefs/structs, which had the identical "predicate ignored" shape.

tests/test_scenarios.py: 212 passed, 0 failed.
</content>
</entry>
<entry>
<title>rpc: stop lying to the driver about renames, modals and teardown</title>
<updated>2026-07-31T07:14:41Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-31T07:14:41Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f720a16f8e4ac538e6b880960fd411357114656d'/>
<id>urn:sha1:f720a16f8e4ac538e6b880960fd411357114656d</id>
<content type='text'>
Five defects found while an agent drove a long RE session over the socket.
Each one was reproduced on a live spawned pane first (an in-process pilot
would not have shown any of them), then fixed:

pane stop truncated the save. `stop` asked the app to quit, slept 400 ms, then
unconditionally killed the pane. Quitting runs App.on_unmount, which writes
every dirty database; a 90 MB .i64 takes tens of seconds, so the kill landed
mid-write and a whole session's annotations went to /dev/null with a cheerful
{"stopped": [...]} on stdout. Now it waits for the pane to actually exit
(--timeout, default 600 s) and only force-kills on timeout, saying so.

The quit verb bypassed the dirty check. It called app.exit() directly rather
than the path a human gets, so the "unsaved changes" logic never ran. It now
routes through _on_quit_choice and reports {saving, dirty}.

Naming a function start from the listing never reached the function index.
`goto &lt;addr&gt;` puts the cursor on the address token, so `n` takes the
name-an-address path, which called bump_items() but left FunctionIndex holding
the old name. Result: the rename response snapshot showed the section label,
and functions()/names()/the palette all reported the rename had not happened —
so a driver that trusts its readbacks redoes work it already did. Twice, in
the session that prompted this. _do_name_addr now updates the index, the nav
stack and the table cell when the address is a function start.

A stripped binary with no entry function started up *inside a modal*.
_auto_land pushed the symbol palette when main() was missing, while ping still
answered ready:true. Every keystroke an RPC driver injected went into the
palette's search box and was silently swallowed. It now lands on the first
function instead and hints at Ctrl+N.

Verbs that inject keystrokes now refuse when a modal is on top, naming it,
instead of failing with "'goto' prompt did not open (word under cursor?)" —
a message that blamed the cursor for what was always a focus problem.

Also: `drive raw` passes k=v values through as strings, so `view lines=8` died
with "'&lt;' not supported between instances of 'int' and 'str'". Numeric params
are now coerced centrally rather than at each call site.

tests/test_scenarios.py: 212 passed, 0 failed.
</content>
</entry>
</feed>
