<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/TODO, branch pre-codemode</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/TODO?h=pre-codemode</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/TODO?h=pre-codemode'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-06T13:11:54Z</updated>
<entry>
<title>README/TODO: literal formats</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=0c7878a708708f8dfe3e0f8c89c1f0bbd672dfea'/>
<id>urn:sha1:0c7878a708708f8dfe3e0f8c89c1f0bbd672dfea</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>blasty@local</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=214076f2e87e69e50600f75723af09d21dcc9ffc'/>
<id>urn:sha1:214076f2e87e69e50600f75723af09d21dcc9ffc</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>blasty@local</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=8188a495b2eed6f120e551e7bda7f16cbe97e952'/>
<id>urn:sha1:8188a495b2eed6f120e551e7bda7f16cbe97e952</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>blasty@local</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=20e34495efd523721990534358e45eb8f6b25c34'/>
<id>urn:sha1:20e34495efd523721990534358e45eb8f6b25c34</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>blasty@local</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=d1deca0826a9e04da195ba3eeb344cd18d38ea17'/>
<id>urn:sha1:d1deca0826a9e04da195ba3eeb344cd18d38ea17</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>blasty@local</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=f4630be2da880011598a3f4d5722e38aedaa636a'/>
<id>urn:sha1:f4630be2da880011598a3f4d5722e38aedaa636a</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>blasty@local</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=07a73abf918711bb776eec9b524bae58dafbd826'/>
<id>urn:sha1:07a73abf918711bb776eec9b524bae58dafbd826</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>
<entry>
<title>TODO: DisasmView removed</title>
<updated>2026-07-26T12:36:12Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T12:36:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=c48652464f021f9a86816dd31be6bd6e644a0fd2'/>
<id>urn:sha1:c48652464f021f9a86816dd31be6bd6e644a0fd2</id>
<content type='text'>
</content>
</entry>
<entry>
<title>listing: syntax-highlight assembly from IDA's own token tags</title>
<updated>2026-07-26T10:50:52Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T10:50:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b59c892c9469eed1a2fc416037500a32d8229259'/>
<id>urn:sha1:b59c892c9469eed1a2fc416037500a32d8229259</id>
<content type='text'>
The listing showed the mnemonic bright and every operand in one body colour.
IDA already classifies each token, for every processor it supports:
generate_disasm_line() emits \x01&lt;tag&gt;text\x02&lt;tag&gt; and the tag says what the
text IS. We were calling tag_remove() and throwing that away.

So: no lexer. A pygments asm lexer would be a worse guess and would need one
dialect per architecture — this is arch-correct for free, including the ARM/MIPS
blobs the loader work just made openable.

  lea rcx, function; "usage"
  insn  reg punct   name   cmt

_idatui_spans() parses the tags into [[kind, text], ...], heads rows carry
"spans", Head.spans holds them, and _span_segments() renders them with a
fallback to the old mnemonic/rest split for older workers.

Palette rule: NEUTRALS for the machine (mnemonic brightest — it's the column you
scan; registers at body weight because they're most of the text), HUES only where
they mean something (numbers, strings, symbols), structure recedes so commas and
brackets stop competing with operands.

Two things that fail SILENTLY and are now encoded:
* The constants are SCOLOR_DATNAME / SCOLOR_CODNAME. There is no SCOLOR_DNAME —
  a wrong guess leaves the tag unmapped, symbols render as plain body text, and
  nothing tells you why. Probed the live IDA to get the real names.
* Spans must be whitespace-collapsed exactly as `text` is, walking characters
  rather than per span, because a run of IDA's column padding straddles span
  boundaries. A row only gets spans when they reconstruct `text` exactly, so a
  mismatch degrades to the old rendering instead of corrupting the line.

The reason this was parked yesterday was NOT a bug in it. listing_view's
"undefining a data head yields an unknown run" waits for
`index_of_ea(dea) &gt;= 0` — but dea is the head it just undefined, so it is in the
OLD model too and the predicate passes instantly, asserting against pre-edit
rows. It only ever passed because the model swap won the race; spans made pages
3x bigger, the swap lost, and the check accused working code. It now waits for
the model to be REPLACED.

Cost measured on libcrypto: 95KB per 500-row page, 50ms; model ensure(2000)
228ms. Acceptable for what it buys.

tests: new asm_highlight scenario (+7) — &gt;90% of code rows carry spans, insn/reg/
punct present, every span kind has a style, spans reconstruct the row text
exactly, mnemonic is the first span. 202/0 scenarios, 26/0 blob, 30/0 project UI.

TODO: DisasmView appears to be dead code (never instantiated; Ctx.dis returns
ListingView), which is why this only needed doing once.
</content>
</entry>
<entry>
<title>asm highlighting: park the work with its findings (not merged)</title>
<updated>2026-07-26T10:43:04Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-26T10:43:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=bc283b66b764da6c38384e30eb834e105db66595'/>
<id>urn:sha1:bc283b66b764da6c38384e30eb834e105db66595</id>
<content type='text'>
IDA already classifies every disassembly token for every processor it supports —
generate_disasm_line emits \x01&lt;tag&gt;text\x02&lt;tag&gt; and the tag says what the text
IS — so this needs no lexer at all, and certainly not a pygments asm lexer, which
would be a worse guess and need one dialect per architecture.

Working implementation is in docs/wip-asm-spans.patch and verified on echo:
"lea rcx, function; \"usage\"" tokenises as insn/text/reg/punct/text/name/cmt,
with sane coverage over 400 rows.

Not merged: it breaks listing_view's "undefining a data head yields an unknown
run" (reports kind=data). Bisected to this change on a fresh database — 10/0 with
it stashed, 9/1 with it applied. The span code doesn't touch `kind`, so the cause
is not yet understood, and I would rather park a working-but-unexplained change
than merge one that makes a real check lie.

TODO records the two non-obvious findings so the next attempt doesn't repeat
them: the constants are SCOLOR_DATNAME/SCOLOR_CODNAME (no SCOLOR_DNAME, and a
wrong guess fails silently as plain body text), and spans must be
whitespace-collapsed by walking characters, not per span, because a run of
spaces straddles span boundaries.
</content>
</entry>
</feed>
