<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/docs, branch pre-codemode</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/docs?h=pre-codemode</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/docs?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>graph: navigate to blocks, not to coordinates</title>
<updated>2026-08-06T14:28:56Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T14:28:56Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=4408eaf2cb814e927f51cb0632e9845994abae51'/>
<id>urn:sha1:4408eaf2cb814e927f51cb0632e9845994abae51</id>
<content type='text'>
Clicking the minimap panned to the exact coordinate under the pointer and moved
the cursor only if a block happened to sit there. Since one minimap cell covers
many canvas cells, "there" was almost always padding: you got a jump into empty
space and the cursor stayed behind, so you had to click a block afterwards to
actually go anywhere.

Blocks cover a few percent of a laid-out graph -- 4.6% of an 87-block function,
0.8% of a 424-block one -- and the rest is the space that keeps edges apart. So
coordinates are the wrong thing to navigate by here. The minimap now snaps to
the nearest block and takes the cursor with it, and a drag scrubs from block to
block. Distance is measured with the column halved, because cells are twice as
tall as they are wide and otherwise "nearest" is not what looks nearest.

A drag-pan or ctrl+d/pageup that ends with no block on screen at all now eases
to the nearest one too, since an empty screen leaves nothing to navigate back
by. It only fires when nothing is visible, so a deliberate pan is never fought.
</content>
</entry>
<entry>
<title>graph: the minimap is clickable, and stops swallowing clicks</title>
<updated>2026-08-06T14:20:29Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-08-06T14:20:29Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=e103f6b6c2399bf46301c3c6f9f5c4cecd456ef8'/>
<id>urn:sha1:e103f6b6c2399bf46301c3c6f9f5c4cecd456ef8</id>
<content type='text'>
Click it to jump the view to that part of the graph, drag to scrub. If the
point you clicked is over a block the cursor lands in it, so the keyboard
carries on from where you pointed instead of snapping back.

This also fixes a real bug rather than only adding a feature. The minimap
FLOATS over the canvas -- it is pinned to the viewport, not drawn into the
graph -- so a click on it was being translated into canvas coordinates and
dropping the cursor into whatever block happened to lie underneath. It has to
be hit-tested before the canvas, which is what on_click now does.

_minimap_rect() is the one source of truth for where it is: the renderer and
the hit-test both take the position from it, so the two-column inset that
keeps it clear of the ScrollView's scrollbar can't drift between them.
</content>
</entry>
<entry>
<title>graph: docs, and the offline layout tools</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=4fbd6b61eaa3461db673b220e2376b3d1e922a9b'/>
<id>urn:sha1:4fbd6b61eaa3461db673b220e2376b3d1e922a9b</id>
<content type='text'>
cfg_dump freezes real CFGs to JSON; graph_spike renders one or --stats a
whole corpus through the shipping engine; graph_smoke is the end-to-end
tool-&gt;domain-&gt;layout check; graph_shot renders the real view headless at a
chosen size, which is the only sane way to judge it (a tiled pane is far
too narrow and the minimap sits on top of the graph).
</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: 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>arm: find Thumb entry points from a vector table (Shift+T)</title>
<updated>2026-07-26T18:49:30Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T18:49:30Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=781c5eff4218d7ffdc9905f652ca08ce10953e43'/>
<id>urn:sha1:781c5eff4218d7ffdc9905f652ca08ce10953e43</id>
<content type='text'>
An ARM function pointer carries the mode in bit 0: odd means Thumb. A Cortex-M
vector table is therefore a list of Thumb entry points, and IDA won't follow them
on a headerless image because nothing tells it those words are pointers at all.
Shift+T scans forward from the cursor and marks them.

  0 functions -&gt; 3 Thumb entries found, 3 disassembled

A word only counts when it is odd, lands in a loaded segment, and its target is
executable and not already data. The even words in a vector table — the initial
stack pointer — fail the first test, which is the point: marking a data word as
code corrupts the listing, so a false positive costs more than a miss. The
fixture includes an even in-range word and an odd OUT-of-range word to keep that
honest.

A note on how this started: I recommended this feature, then probed
experiments/fibonacci.bin for the signal and found ZERO odd in-range pointers —
it's a flat code blob, not a firmware image. Rather than build a detector I
couldn't test, I wrote experiments/cortexm.bin: a real vector table pointing at
small self-contained Thumb handlers. The first version of that fixture aimed its
handlers into the middle of copied code, so two "entries" were really inside one
function — the tool was right and the fixture was wrong, which is worth stating
because I nearly filed it as a bug.

Function creation goes through one _idatui_add_func helper now, shared with
define_func_run: add_func(ea) alone fails on freshly-marked code (IDA can't find
the end), and the scan hit exactly the same wall `p` did.

Status precedence, fixed properly this time. An action's result kept being
overwritten by the reload it triggered — cursor moved, filter re-applied,
functions re-counted. I patched that at FIVE separate call sites before
admitting it's one problem. _status(text, priority=True) now marks a result: it
holds the bar for 8s or until the next keypress, and routine chatter can't
outrank it. The per-site special cases are gone.

tests: +4 thumb (20) — a bare vector table gives IDA nothing, scanning finds
exactly the three handlers, the non-pointer words are ignored, and the result
survives both the reload and the reindex. 209/0 scenarios, 30/0 blob, 30/0
project UI.
</content>
</entry>
<entry>
<title>arm: offer 32-bit ARM at load, and fix `p` on carved code</title>
<updated>2026-07-26T13:04:51Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T13:04:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=2c2124aff2f4ed6df23512603b6e1ecdcd4b699b'/>
<id>urn:sha1:2c2124aff2f4ed6df23512603b6e1ecdcd4b699b</id>
<content type='text'>
Reported as "after c a few times and p at the entry point, Tab just flashes and
nothing decompiles". Three separate things, found by following it down:

**1. `p` failed on hand-carved code.** ida_funcs.add_func(ea) asks IDA to find
the function's end and on carved code it often can't — a run ending in a tail
call, or whose last instruction isn't recognised as a return, fails with no
reason given. add_func(ea, end) with an explicit end succeeds. define_func_run
tries IDA's way first, then falls back to the end of the contiguous instruction
run, and says which it used.

**2. The database was 64-bit, so Hex-Rays refused it regardless.** Bare `-parm`
gives an AArch64 database. Ask Hex-Rays for the failure object rather than
reading None as "dunno" and it says exactly what's wrong: "only 64-bit functions
can be decompiled in the current database". So the disassembly looked right and
F5 could never work.

That is decided at LOAD and cannot be corrected — inf_set_app_bitness(32)
afterwards makes the decompiler INTERR 50735. The fix is at the load dialog:
arm:ARMv7-A (most firmware), arm:ARMv7-M / arm:ARMv6-M (Cortex-M, Thumb only)
and arm:ARMv5TE now sit alongside 64-bit `arm`, labelled with their bitness.
With arm:ARMv7-A, experiments/fibonacci.bin decompiles:

  void __fastcall __noreturn sub_0(int a1) { int v2; v2 = sub_E3C(a1, 0); ... }

— and IDA's own auto-analysis finds 54 Thumb functions on load, versus none as
plain `arm`.

**3. `t` was silently building an undecompilable state.** It forced the SEGMENT
to 32-bit in a 64-bit database, which produces correct-looking disassembly that
F5 will never touch. It now says so and names the fix (Ctrl+L, arm:ARMv7-A)
rather than leaving you to discover it.

tools/verify_procs.py now reports each processor's resulting bitness, since that
is the reason the variants exist — and it compares against the base module name,
because a variant reports "ARM".

tests: test_thumb_ui.py +5 (13 total) — a 64-bit database warns and names the
fix, a 32-bit one finds functions by itself, Tab decompiles a Thumb function and
the result reads like C. test_formats.py +2 (34) pinning that a 32-bit variant is
offered and the ARM labels state their bitness. 209/0 scenarios, 26/0 blob, 30/0
project UI.
</content>
</entry>
<entry>
<title>arm: switch ARM/Thumb decoding with `t`</title>
<updated>2026-07-26T12:51:07Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T12:51:07Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b'/>
<id>urn:sha1:7bdf3741c91f76bfff3f3e054a5cf41b7f476d0b</id>
<content type='text'>
`c` could not carve Thumb code. Thumb isn't a property of the bytes — it's a
mode the CPU is in — so a raw image gives IDA nothing to detect: at a Thumb entry
point it decodes 16-bit instructions as 32-bit ARM and produces confident
nonsense. experiments/fibonacci.bin starts with `08 b5` = push {r3,lr}, which
IDA reads as SVCLT 0xBF00.

`t` on the listing switches the mode at the cursor and disassembles in it:

  Thumb @ 0x0 (segment set to 32-bit; Thumb needs ARM32) — 10 instructions
  0x0 PUSH {R3,LR}   0x2 MOVS R1, #0   0x4 MOV R4, R0   0x6 BL unk_E3C

Setting the T segment register is only half of it. Thumb does not exist in
AArch64, and a headerless blob loaded with -parm comes up 64-bit, so T alone
changes nothing and looks broken — I watched exactly that happen while probing
the API. Asking for Thumb IS asking for ARM32, so set_thumb forces the segment
to 32-bit and says so rather than doing it silently.

It also has to del_items over the range first: the bytes are currently decoded
in the old mode, and leaving that item defined pins the wrong instruction length
so the new mode has nothing to apply to.

Implemented as a `thumb` kind in the existing edit-item flow, so it inherits the
shared reload — same cache bump, same ViewAnchor restore, same status flash. It
switches AND disassembles, because flipping T and leaving the bytes undefined
shows you nothing and reading the code was the point.

tests: new tests/test_thumb_ui.py (8) driving the real Thumb binary — `c` alone
does NOT produce the prologue, `t` does, the instructions are 16-bit wide (in ARM
mode those three rows would be one 4-byte instruction), the run continues, the
status explains the 32-bit forcing, and `t` toggles back. Deletes the .i64 first,
because T and the segment's addressing mode are saved in it and a stale database
would answer the question for us.

209/0 scenarios, 26/0 blob, 8/0 thumb.
</content>
</entry>
</feed>
