<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/TODO, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/TODO?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/TODO?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-21T10:24:17Z</updated>
<entry>
<title>TODO: change notifications and discard-changes landed with PR #1</title>
<updated>2026-08-21T10:24:17Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:24:17Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=73955f4efd6648a794aecd7ba413dc283a814baf'/>
<id>urn:sha1:73955f4efd6648a794aecd7ba413dc283a814baf</id>
<content type='text'>
</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>
<entry>
<title>Rebase MISTER EXO's ida-codemode port onto the current tree</title>
<updated>2026-08-07T10:40:14Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-07T10:39:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=3fb2c23b9c3792708392c5857ef63e8190aa86f5'/>
<id>urn:sha1:3fb2c23b9c3792708392c5857ef63e8190aa86f5</id>
<content type='text'>
Mechanical part of the port: the 27-file patch was cut against a base ~148
commits behind us, so it did not apply. Resolved 11 conflicts (all of them
diff drift, not semantic clashes) and the three file deletions:

- app.py: the patch re-inserted _do_rename/_do_name_addr/_seek_split etc. as
  "theirs" because our tree moved them to edit_ctl.py/trace_ctl.py. Kept ours
  and applied the real intent (WorkerClient-&gt;CodeModeClient, .call-&gt;.invoke,
  _open_worker_client-&gt;_open_database_client) at their current homes.
- domain.py: kept Head as a NamedTuple -- the patch reverted it to a frozen
  dataclass, which the perf work measured at 2.9us vs 1.9us per row on a
  quarter-million-row walk. Dropped _fetch_output (no download_url under Code
  Mode) and its now-dead urllib/json imports.
- pane.py: the patch's deletion swallowed our zellij support along with the
  worker-reaping block it meant to remove. Kept zellij, removed the reaping.
- test_scenarios.py: the idb_save-&gt;save_database teardown hunk belongs to
  tests/_fixtures.py in our tree; applied it there and kept our pc_num_format
  scenario that the drift landed on.

Three defects in the patch itself, fixed here:

- It made "import idatui" hard-require ida_codemode, so every offline suite
  died at import -- including the pure ones (graph/index/trace) that are the
  house rule for "tests/run.py --fast". The import is now deferred and gated
  on the binding, which is also what lets the port's own contract tests
  inject a fake DatabaseHandle.
- project.stage() inlined an ida_codemode.registry import and treated "library
  not installed" as "someone owns this database", which broke IDA-free project
  staging. Ownership lookup moved to codemode_client.database_owner().
- tests/test_codemode_client.py had no NEEDS_IDA marker, which tests/run.py
  rejects outright.

Offline suite: 301 passed, 0 failed. Against master's 344 the whole delta is
accounted for: -40 worker_client (module deleted), -18 launch sweep checks
(behaviour deliberately removed) +3 guarding that it stays removed, +2 pool
(GUI-save semantics), +13 new codemode_client contract tests.

NOT yet done, and the port is not functional without it: the adapter is
missing five operations our tree grew since the patch's base (flowchart,
op_format, pc_nums, pc_num_format, survey_binary) and its "heads" predates
back-walking and digest/expect.
</content>
</entry>
<entry>
<title>README/TODO: literal formats</title>
<updated>2026-08-06T13:11:54Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</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=91e64d916c40e77e0ff43ed7cd190c1a59f6193f'/>
<id>urn:sha1:91e64d916c40e77e0ff43ed7cd190c1a59f6193f</id>
<content type='text'>
</content>
</entry>
<entry>
<title>trace: a stale navigation no longer drags the view back</title>
<updated>2026-07-30T11:37:46Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-30T11:37:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=9a77817f82d43056ba3d8049deb1ecb7a65103ee'/>
<id>urn:sha1:9a77817f82d43056ba3d8049deb1ecb7a65103ee</id>
<content type='text'>
Fixes the bug found while building M3. Navigations run in workers and are applied
when they land; the trace's OPENING seek goes to t=0, which for a normal binary
is _start, and that navigation is slow. It arrived after later seeks and won,
leaving the cursor and _cur on _start while the trace's pc was elsewhere — and it
never settled, measured stable for 3+ seconds. Anything cursor-based done just
after a seek (`&gt;` asks about the address under the cursor) then acted on the
wrong address.

The decompiler path has had a staleness guard since 756589a; the listing path
never got one. It has one now (_open_at_if_current), and a seek bumps _nav_seq so
older in-flight navigations are dropped.

Verified both directions on the exact reproduction: seek to the first execution
of a repeated instruction, seek to the second, wait — cursor stays put with the
guard, and with the guard removed it drifts to 0x34d0 (_start) exactly as
reported.

Scope, deliberately narrow. I first bumped _nav_seq in _goto_ea for EVERY
navigation, which is the more general rule, and a full run then failed
follow_xrefs — a follow can be dropped by whatever navigates next. That check has
flaked before so it is not proof, but the mechanism is real and my evidence is
only about seeks, so the bump lives in _seek. TODO records what would justify the
general version and what test it needs.

tests: +1 trace UI (39) — seek, seek again, wait 3s, and the cursor is still on
the instruction the trace is at. Two consecutive full runs 212/0 after narrowing.
</content>
</entry>
<entry>
<title>trace: seek verbs — next/previous execution, and who set this register</title>
<updated>2026-07-29T21:39:58Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-29T21:39:58Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=495d52a1599d5ad02c2ca932b558559c1920598e'/>
<id>urn:sha1:495d52a1599d5ad02c2ca932b558559c1920598e</id>
<content type='text'>
M3. Stepping walks time; seeking jumps to the next time THIS thing was touched,
which is what makes a trace more than a very long single-step log.

`&gt;` / `&lt;` — next/previous execution of whatever the focused view addresses. One
pair of keys, two questions, because what's on screen already says which:

* listing: the instruction under the cursor. "When else did this run?"
* pseudocode: the whole C line, as the union of its instructions' executions. A
  line is not one address, and falling back to its single /*ea*/ marker would
  answer a narrower question — usually none at all, since most lines have no
  marker.
* hex: the byte under the cursor, via memory_accesses.

It says where you landed ("execution of 0x3160: 2 of 2 @ t=320") and, at either
end, that you're AT the end rather than silently doing nothing — a key that does
nothing is indistinguishable from a broken one.

`W` — the registers with the instruction that set each to its current value, and
the distance back. Enter seeks to that write, f seeks forward. Backward is the
direction people want: you notice a bad value after it has been used. This is
the question a trace exists to answer and it was already in the model
(last_write/next_write), untested in anger until now.

tests: +13 trace UI (38) — &gt; and &lt; move between the two executions of a
repeated instruction, the status names which execution it is, both edges report
instead of moving, W opens, and choosing a register lands on an instruction that
REALLY wrote it (checked against the trace's own changed-set, not just the
timestamp matching).

Two things the tests taught me, both recorded:
* focus() does not make a view active outside split mode — Tab does. My first
  seek test pressed &gt; while _active was still "decomp", so it asked the
  pseudocode about a line with no instructions.
* TODO gets a new entry: a stray late navigation to the entry function arrives
  after a seek and wins, leaving the cursor on 'start' while the pc is
  elsewhere. Same shape as the stale-decomp-result bug fixed in 756589a, which
  got a sequence guard the listing path never did.

212/0 scenarios, 35/0 model, 12/0 differential.
</content>
</entry>
<entry>
<title>app: one instruction map for the decompiled function, not two</title>
<updated>2026-07-28T14:17:21Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-28T14:17:21Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=04c6f3350dfd25d2d5e1e3f6da41fe515874fada'/>
<id>urn:sha1:04c6f3350dfd25d2d5e1e3f6da41fe515874fada</id>
<content type='text'>
Finishing the thing the last commit only worked around. The split view and the
trace path each kept their own per-line instruction map of the same
pseudocode — fetched separately, indexed separately, and keyed differently: the
split one on _cur (the function the CURSOR is in), the trace one on the function
the DECOMPILER has loaded. Those are not the same thing, which is precisely how
the two ended up describing different functions and why I spent a commit
chasing a "sparse decomp_map" that was never sparse.

_apply_split_map now indexes once and both read it. Keyed on the decompiler's
loaded function, and no longer conditional on split being on — the old guard
dropped the result whenever _cur had moved while the fetch was in flight, which
during stepping is almost always.

Measured after: three decomp_map fetches across 28 steps (two for main, one for
the function stepped into), the split map and the trace map are literally the
same object, and both describe what is on screen.

Process note, because this is the second time: my first attempt at this edit
SILENTLY DID NOTHING — the pattern didn't match (a duplicated comment line I'd
mangled), the old method stayed, and the new caller hit its `not self._split`
guard, so the painting tests went from passing to "0 lines". Same failure mode
as the key bindings that never got added. Structural edits now assert that the
anchor was found and that the replacement is present before writing.

212/0 scenarios, 26/0 trace UI, 30/0 project UI, 20/0 split view.
</content>
</entry>
<entry>
<title>trace: stop the decompiler thrashing during a step (and correct the record)</title>
<updated>2026-07-28T13:16:34Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-28T13:16:34Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=39b8fa97498a558ce1701900bb180fef15e8a7a6'/>
<id>urn:sha1:39b8fa97498a558ce1701900bb180fef15e8a7a6</id>
<content type='text'>
I blamed decomp_map in the last commit. It was innocent: called directly it
returns 769 lines, 475 with addresses, for exactly the function I said it
returned four for. The four-line map belonged to a PLT stub the decompiler had
momentarily switched to, and I sampled mid-bounce.

The actual fault: _seek_split decided "has execution left the decompiled
function?" from _split_range, which is maintained by a guarded async path
(_apply_split_map drops its result if _cur moved while in flight) and therefore
lags during stepping. A stale range made every step look like a function change,
so the decompiler bounced main -&gt; stub -&gt; main, each bounce paying a synchronous
769-line map fetch on the UI thread.

Now the decision comes from the map the trail painting already holds, keyed to
what the decompiler currently HAS loaded. The bouncing is gone — three map
fetches across twelve steps instead of one per step — and the pseudocode cursor
follows every instruction the decompiler attributes to a line, including across
a call into another function.

What it does NOT do: guess. Roughly half of a function's instructions have no
line attributed, and the obvious fallback (nearest mapped address at or before
the pc) is unsound — C lines are not monotonic in address, and it put an
instruction early in main on line 708, "sub_2040();", near the end. The cursor
waits instead; the trail still marks where you are.

tests: +1 trace UI (26) — over ~28 steps, every instruction that IS mapped is
followed by the pseudocode cursor. 212/0 scenarios.

TODO corrected: the entry blaming decomp_map now says what actually happened,
including that _split_ea2line/_split_range are still fed by the laggy path and
remain a latent issue for the split view's own sync.
</content>
</entry>
<entry>
<title>trace: a step in split view moves the listing cursor to the pc</title>
<updated>2026-07-28T13:06:31Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-28T13:06:31Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=6b438a74ae5edbb26026deef5e4652b7eb743318'/>
<id>urn:sha1:6b438a74ae5edbb26026deef5e4652b7eb743318</id>
<content type='text'>
Normal navigation moves one pane and gives the companion a band, never a cursor
— that rule exists so the two can't chase each other. A trace step isn't
navigation: time is a single global position and both panes are showing the same
instant, so the cursor belongs on it in both.

_seek_split places the listing cursor on the current instruction, then hands off
to the existing _sync_split so the companion still gets its band and align() at
the driver's screen row. The anchoring machinery is used, not bypassed.

PARTIAL, and the shortfall is worth stating plainly: the LISTING cursor tracks
the pc reliably (tested over consecutive steps). The PSEUDOCODE cursor only
follows when decomp_map covers that address, and for cat's main it covers almost
nothing — four entries for a 700-line function. That is not something this
commit introduced and not something I could fix responsibly without
understanding it; TODO has what I measured, including that dec.goto(96) left the
cursor at 0 in the same run, which may or may not be the same bug.

One real fix along the way: _place_decomp_at prefers the map the trail painting
keeps (keyed to the decompiler's currently loaded function) over the split
view's _split_ea2line. The latter is refreshed by a guarded async path that
drops its result if _cur moved while in flight, and a burst of steps moves _cur
constantly — so during stepping it is frequently a map of the function you just
left.

tests: +2 trace UI (25) — stepping in split moves the listing cursor onto the pc
for six consecutive steps, and the trail marks it 'now' in both panes. 212/0
scenarios.
</content>
</entry>
<entry>
<title>tests: run the scenario suite on a scratch copy, not the tracked target</title>
<updated>2026-07-26T19:02:47Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-07-26T19:02:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=ad9bc359f4bca82d8a9b48beb137d2acf544f1da'/>
<id>urn:sha1:ad9bc359f4bca82d8a9b48beb137d2acf544f1da</id>
<content type='text'>
The suite edits the database — defines code, undefines items, renames, comments —
and IDA saves all of it. Running that against targets/echo.i64 meant every run
inherited the last one's damage.

That cost real time twice. decomp_follow_self "started failing" with no code
change, and stayed failing until the .i64 was deleted; an edit-position check
looked flaky about one run in three and I nearly reported it as an async race.
Both were the database drifting. A suite whose result depends on its own history
cannot be trusted to accuse the code — and it had been quietly laundering bad
conclusions for however long.

Now the suite copies the binary into a temp dir and seeds it from a golden
database (&lt;target&gt;.pristine.i64) that nothing ever writes back to. Every run
starts from identical bytes; the tracked target is never opened.

The golden copy is built once, on first run, by analysing and saving before any
scenario runs — so it costs one analysis rather than one per run. Rebuilt
automatically if the binary is newer.

Verified: two consecutive full runs both 209/0; targets/echo.i64 no longer
exists after a run; a deliberately corrupted targets/echo.i64 is ignored
completely (11/0 with junk in place, and the junk untouched afterwards); no temp
directories leak.

The other suites were already clean for the same reason, by different means:
test_blob_ui builds a throwaway binary, test_project_ui stages copies, and
test_thumb_ui deletes the .i64 before each phase because the T flag and the
segment's bitness are saved in it.
</content>
</entry>
</feed>
