<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/drive.py, branch pre-codemode</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/drive.py?h=pre-codemode</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/drive.py?h=pre-codemode'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-06T22:07:22Z</updated>
<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>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>drive: a note went to address 0, and a slow edit was reported as a failure</title>
<updated>2026-08-01T16:20:13Z</updated>
<author>
<name>agent</name>
<email>agent@local</email>
</author>
<published>2026-08-01T16:20:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=5f5c2ba0621e53ae380dc434e578c6f67e9b8deb'/>
<id>urn:sha1:5f5c2ba0621e53ae380dc434e578c6f67e9b8deb</id>
<content type='text'>
Two bugs found driving a 65KB ARM firmware image (one flat 42k-line listing, no
ELF sections to break it up):

`note` did goto + `cursor line=0` before commenting. Line 0 is the top of the
function only in the DECOMPILER; in the listing it is the top of the SEGMENT, so
every note landed at address 0 -- and scrolling a 42k-line listing there took so
long the call timed out, which read as "comments are broken". goto already lands
on the function's first line, so the cursor call just goes; note now also
reports where it landed.

The client's 90s timeout was too tight for the same reason: comments on that
listing take 26-106s (the rebuild has no function boundary to stop at), so the
CLI reported "no response ... server busy or the op is hung" for edits that had
already been applied. Believing a successful edit failed is the worse error --
the driver redoes it, or "fixes" what was never broken. Default is now 300s;
IDATUI_RPC_TIMEOUT still overrides.
</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>projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs</title>
<updated>2026-07-25T21:03:10Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:03:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b8d9aa4fce78f4523cffaba8b46095d2dfb66310'/>
<id>urn:sha1:b8d9aa4fce78f4523cffaba8b46095d2dfb66310</id>
<content type='text'>
Three things, all following from phase 3 making cross-binary jumps ordinary.

**A cross-binary jump was a one-way door.** Nav history is per-binary, so
arriving in another binary — a project search hit, or now following an import
into the library that implements it — landed you in an empty history with nothing
to take you back. _switch_then_goto records the binary it came FROM, and
action_back falls through to that hop once local history is spent: Esc walks back
through the function you were in, then the binary you were in. Manual Ctrl+O
switching records nothing, because that isn't navigation.

**Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms
the binary providing the most of this one's imports — where a follow is most
likely to go, so its startup is paid before you ask for it. "Next in the list"
would have been arbitrary; phase 3 gave us something better to ask.

pool.prewarm() refuses rather than making room. Evicting a binary the user
visited to speculatively load one they haven't is a straight downgrade, and it
throws away that binary's caches as well; at a tight budget pre-warm just does
nothing. The cost of a worker that doesn't exist yet can only be estimated, so it
uses the largest resident one (same program, different database) — and if that
estimate proves wrong, the speculative worker is the one evicted, never a chosen
one.

**Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists
the inventory (active / resident / indexed / where Esc returns to) and `switch
{binary,addr?}` makes another active — with an address it takes the search-hit
path, so it records a hop. state gains `binary` and `hops`, which it should have
had the moment project mode existed.

Verified on real sessions: drive binaries/switch against an echo+cat project
pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc
(provider of echo's imports) and warming it after an evict.

tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts
nothing when refusing, ignores unknown labels) and +4 project UI (jump records
the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with
the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool,
33/0 project.

Left open: project-level persistence across sessions.
</content>
</entry>
<entry>
<title>drive: make `dis` drive the live UI too (generalize _show_decomp -&gt; _show_view)</title>
<updated>2026-07-11T19:09:45Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-11T19:09:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b55e5529fb96e0cfa5769d1af4e89350c8c255ce'/>
<id>urn:sha1:b55e5529fb96e0cfa5769d1af4e89350c8c255ce</id>
<content type='text'>
</content>
</entry>
<entry>
<title>drive pc: render pseudocode in the TUI, not just the driver</title>
<updated>2026-07-11T08:09:03Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-11T08:09:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=156168cb42237884edd39af15bc80138c9443910'/>
<id>urn:sha1:156168cb42237884edd39af15bc80138c9443910</id>
<content type='text'>
The pc command called only the read-only pseudocode RPC (runs off the UI
loop, never touches the screen), so LLM-driven sessions showed nothing on
the live pane. Compose goto + toggle_view + search so the real TUI
navigates to the function, makes the decomp pane the visibly-active view,
and jumps the cursor to the needle — then return the same text as before.
</content>
</entry>
<entry>
<title>drive: add save + retype; point docs/skills at the ergonomic frontend</title>
<updated>2026-07-10T14:21:36Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-10T14:21:36Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=a03a1e08f65806f8963eeffc5f65c28af40bc949'/>
<id>urn:sha1:a03a1e08f65806f8963eeffc5f65c28af40bc949</id>
<content type='text'>
Round out idatui.drive with 'save' and 'retype &lt;fn&gt; &lt;proto&gt;' so the whole
common RE loop (orient/understand/act/persist) has a terse command. Update
docs/RPC.md and both skills (idatui, idatui-rpc) to recommend idatui.drive
as the day-to-day driving surface, with rpcclient/raw as the fallback for
unwrapped verbs.
</content>
</entry>
<entry>
<title>drive: ergonomic RE helper over the RPC socket (terse text, auto socket)</title>
<updated>2026-07-10T14:17:50Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-10T14:17:50Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=33bd85e41221a82a8ee7b71d39eda59d89b78ee7'/>
<id>urn:sha1:33bd85e41221a82a8ee7b71d39eda59d89b78ee7</id>
<content type='text'>
Driving via rpcclient meant re-exporting the socket every fresh shell,
long invocations, and piping ~every call through python -c to pull one
field or grep pseudocode. idatui.drive fixes that: it auto-resolves the
socket (the single live pane from the registry), prints compact text
instead of JSON, and bundles the common gestures:

  where | go | pc &lt;fn&gt; [substr] | dis &lt;fn&gt; [n] | callees/callers &lt;fn&gt; |
  names &lt;substr&gt; | rename &lt;old&gt; &lt;new&gt; | mv old=new... | note &lt;fn&gt; &lt;text&gt; |
  screen | raw &lt;method&gt; k=v

So 'goto+rename+parse' becomes 'drive rename old new', and 'pseudocode |
python -c grep' becomes 'drive pc fn needle'. rpcclient stays the raw
transport. rpc_smoke drives it end-to-end (29 green).
</content>
</entry>
</feed>
