<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/launch.py, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/launch.py?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/launch.py?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-07-25T22:00:19Z</updated>
<entry>
<title>loading: ask how to load an unrecognised file, like IDA does</title>
<updated>2026-07-25T22:00:19Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T22:00:19Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=ba0b26ebb0ce16632e8a2e67675dd79a11e26551'/>
<id>urn:sha1:ba0b26ebb0ce16632e8a2e67675dd79a11e26551</id>
<content type='text'>
Last commit let you SAY how to load a blob. This one notices when you should
have. Interactive IDA pops a dialog when no loader matches; we silently loaded
as x86 at 0 and analysed to nothing, so the flag only helped people who already
knew they needed it — which is exactly the people who don't need help.

formats.sniff() recognises the formats IDA definitely handles (ELF, PE, Mach-O,
dex, wasm, ar, COFF, Intel HEX, S-records). Anything else gets
LoadOptionsScreen: a filterable processor list with human labels, a load-address
field, Enter to accept, Esc to load it the way IDA would have anyway.

Deliberate asymmetry: the sniff only claims formats it is sure about. A false
"unknown" costs one dismissible dialog; a false "known" is the silent wrong
answer this exists to kill. Esc is always an escape hatch.

The list offers 20 processors, not IDA's 73 — most of the rest are museum
pieces, and a name typed into the filter that matches nothing is taken literally
so nothing is actually unreachable. Endianness is spelled out (arm vs armb)
because getting it backwards is the most common route to zero functions.

Asked only when nobody has answered yet: not with --processor, not in project
mode (entries carry their own), and not when a database exists — the .i64
already records how the image was loaded.

Textual trap worth recording: the screen stored the file size in self._size,
which is Widget's own backing field for outer_size. Assigning an int to it
crashes layout with "'int' object has no attribute 'region'" from deep inside
_set_dirty, nowhere near the cause. Same family as the _render collision.

The paragraph conversion now lives in exactly one place (formats.load_args);
BinaryRef and launch both call it.

Verified on a real AArch64 blob: dialog appears, filtering to "arm" leaves two
entries, base 0x8000000 accepted -&gt; "-parm -b800000" -&gt; 35 functions at
0x8002440. An ELF never asks, and neither does a blob that already has a .i64.

tests: new tests/test_formats.py (21) and a load_options scenario asserting the
dialog stays out of the way for a recognised binary. 199/0 scenarios, 39/0
project, 30/0 project UI, 36/0 index, 27/0 pool, 21/0 formats.
</content>
</entry>
<entry>
<title>loading: say how to read a headerless blob (processor, base)</title>
<updated>2026-07-25T21:49:14Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:49:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=57b09047881d4bb42b2b98e4bb0739d5bc5e5af8'/>
<id>urn:sha1:57b09047881d4bb42b2b98e4bb0739d5bc5e5af8</id>
<content type='text'>
A raw firmware dump has no format to detect, so IDA fell back to x86 at address
0. It doesn't fail — it opens, analyses, and finds nothing. An AArch64 image
loaded this way gave 0 functions; told the truth it gives 35.

    ida-tui fw.bin --processor arm --base 0x8000000

and per binary in a project, which is what a multi-image firmware actually
needs:

    {"path": "app.bin", "processor": "arm", "base": "0x8000000"}

idapro.open_database() already accepted IDA command-line switches; nothing was
passing any. Plumbed BinaryRef -&gt; WorkerPool -&gt; WorkerClient -&gt; worker argv, plus
a load_args for the single-binary path that has no project ref.

base is written the way people say it (0x8000000, int or string, any base).
IDA's -b is in PARAGRAPHS — -b1000 loads at 0x10000 — so BinaryRef.load_args
converts, and a base that isn't 16-byte aligned is refused rather than silently
landing 16x off. ida_args passes anything else through.

Two bugs found by testing the whole path rather than the happy one:

* Project.load() whitelisted path/label when normalising entries, so the load
  options were dropped the first time a project was reopened — set a processor,
  come back tomorrow, it's gone.
* Re-passing the switches to an EXISTING database makes IDA refuse the open
  (rc != 0, no functions). The .i64 already records how the image was loaded, so
  the worker skips them once a database exists. My first guard checked
  splitext(path) + ".i64" and never fired, because IDA names it "&lt;file&gt;.i64" —
  keeping the extension. It checks both spellings now.

Verified on a real AArch64 blob: fresh load 35 functions based at 0x8002440,
reopen 35 again, and the CLI rejects an unaligned or non-numeric --base.

tests: +6 project (options recorded, paragraph conversion, file round-trip,
add() takes them, an ELF passes nothing, hex-string base). 39/0 project, 195/0
scenarios, 30/0 project UI, 36/0 index, 27/0 pool.
</content>
</entry>
<entry>
<title>projects: don't duplicate a binary that's already in the project</title>
<updated>2026-07-25T14:26:40Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T14:26:40Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=391422bc805fb8620a77cc3f82787da989f1b2cf'/>
<id>urn:sha1:391422bc805fb8620a77cc3f82787da989f1b2cf</id>
<content type='text'>
`--project fw.json a.elf b.elf` where those are already listed appended them
again, so the project grew a second copy on every launch — each duplicate then
staged its own file, opened its own worker and got its own index entries.

Dedupe on the RESOLVED SOURCE PATH, which is the only identity that's actually
correct here: two different foo.elf from different directories are different
binaries and must both be accepted (the label disambiguator already gives them
foo.elf and foo.elf_2), while ./a.elf, /abs/a.elf and a symlink to it are all the
same file and must collapse to one entry.

* Project.by_source(path) — lookup by realpath.
* Project.add() returns the existing entry instead of appending a duplicate.
* Project.create() drops repeats on one command line too.
* launch.py reports what it did: "added N binary(ies)" / "N already in the
  project (matched by path) — left alone", and only rewrites the file when
  something actually changed.

Not deduped in _build_refs on load: remove() maps refs to entries by index, so
collapsing there would desync them, and a hand-edited duplicate still works
(labels disambiguate).

tests/test_project.py +8 checks: re-add is a no-op, so are a relative spelling,
a messy ../ path and a symlink; a same-named file from another directory IS
added and gets a distinct label; create() drops repeats. 33/33. Verified on the
real CLI: re-running the reported command leaves the project at 3 entries.
</content>
</entry>
<entry>
<title>projects: switch between a project's binaries in the TUI (phase 1c)</title>
<updated>2026-07-25T09:51:38Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T09:51:38Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb'/>
<id>urn:sha1:f4cb6eba274c411c503cbc8b4ed10fdfc6b70dcb</id>
<content type='text'>
Wires the project model + worker pool into the app. Project mode is ADDITIVE —
without --project the app is exactly the single-binary tool it was, which is what
keeps the 167-check pilot meaningful.

* IdaTui(project=...) builds a WorkerPool and opens the project's first binary;
  _open_worker_client asks the pool instead of spawning directly.
* BinaryState snapshots what a switch leaves behind (program, func_index, nav,
  cur, view prefs, filter). Switching reuses the _after_reconnect shape: swap
  client+program, rebuild the index, reopen the entry. A still-resident binary
  restores instantly (Program + index are in memory); an evicted one gets a fresh
  worker but keeps its nav history, which is just addresses.
* ProjectPalette (Ctrl+O, + a "Switch binary…" palette command): the project's
  binaries with resident/analysed/pinned/active state and memory, filterable.
* launch.py --project FILE, creating the project when binaries are also given;
  stages everything up front so the source tree is never written to.

Two bugs found while testing:
* _did_auto_land is app-wide, but landing is per-binary: after the first binary
  landed, a cold switch never landed at all AND left the switch overlay up
  forever. Reset it per switch.
* PRE-EXISTING: the status Static had Textual markup enabled, so a single-word
  bracket marker parses as a style tag and is silently eaten — [listing] and
  [pseudocode] have never actually rendered (only [split · listing] survived,
  because the · makes it an invalid tag). Status is plain text with brackets and
  symbol names, so markup=False.

tests/test_project_ui.py: end-to-end pilot on two real binaries (18 checks) —
boot, switcher contents, switch, per-binary index, both workers resident,
switch back with state intact, return-to-where-you-were, and the promise that
the source tree stays pristine while every artifact lands in the sidecar.
Full single-binary suite unchanged at 167/2 (the standing flakes).
</content>
</entry>
<entry>
<title>mcp: collapse app + launcher to worker-only</title>
<updated>2026-07-24T12:45:28Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-24T12:45:28Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=720f9e8283257e21ed183d8dff84c7ab6777314d'/>
<id>urn:sha1:720f9e8283257e21ed183d8dff84c7ab6777314d</id>
<content type='text'>
The idalib worker is now the sole backend for opening a binary, so remove the
ida-pro-mcp code paths from the hot path:

* app.py: IdaTui.__init__ drops url/db/ensure_server/backend (now just
  open_path/keepalive/rpc_path/ttl); _connect calls _open_worker_client directly;
  _open_mcp_client deleted; _reconnect respawns the worker only; self.client and
  _after_reconnect typed WorkerClient. No more IDAClient import.
* launch.py: rewritten worker-only -- validate the binary, sweep stale locks,
  spawn the TUI (which starts the private worker behind its overlay). The whole
  supervisor dance (_ensure_server/_start_supervisor/_open_binary/
  _existing_session) is gone; `ida-tui foo.elf` is the one usage.
* tests/test_scenarios.py: pilot is worker-only (run(binary); binary via
  positional/--worker/--binary, defaults to targets/echo).

Verified: app + launch + pilot import and construct; pilot lists 31 scenarios.
The mcp modules (client.py/pane.py/tui.py/spawn.sh) still exist as dead code and
are deleted in the next commit. Worker pilot (134/2-known-flakes) still the gate.
</content>
</entry>
<entry>
<title>deprecate: make the idalib worker the default backend; mark mcp path for removal</title>
<updated>2026-07-23T23:56:47Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T23:56:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=03564655fda1682e61bbf7055c3ef310c448cf28'/>
<id>urn:sha1:03564655fda1682e61bbf7055c3ef310c448cf28</id>
<content type='text'>
Opening a binary now defaults to our own idalib worker; the ida-pro-mcp HTTP
supervisor path is deprecated (kept only for --db/attach and --backend mcp).

* launch.py: --backend default resolves to worker for a fresh binary open, mcp
  for the attach modes (--db / bare `ida-tui`, which have no worker equivalent);
  explicit --backend or IDATUI_BACKEND still wins. Logs a deprecation notice when
  the mcp path is used.
* Deprecation markers on client.py and server/patch_server.py; the ida-tui shell
  header and README now describe the worker as primary and note
  $IDATUI_WORKER_PYTHON. TODO tracks the removal checklist.

No code deleted yet — the mcp fallback stays until the worker is proven on a box
where idalib can spawn (pilot against --backend worker is the gate). Backend
resolution matrix verified: `ida-tui bash`-&gt;worker, bare/`--db`-&gt;mcp, explicit
flag/env honored.
</content>
</entry>
<entry>
<title>app: --backend {mcp,worker} — run the TUI on our idalib worker (migration step 3)</title>
<updated>2026-07-23T23:37:48Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T23:37:48Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f925701e003f05160d46cc3efd8eacf3ea256aeb'/>
<id>urn:sha1:f925701e003f05160d46cc3efd8eacf3ea256aeb</id>
<content type='text'>
Wires WorkerClient into the app as a selectable backend, so you can launch:

    ida-tui --backend worker /path/to/binary        # or IDATUI_BACKEND=worker

* IdaTui gains a `backend` param. _connect is refactored into _open_mcp_client()
  (the existing ida-pro-mcp path, unchanged) and _open_worker_client() (spawns a
  private idalib worker via WorkerClient that opens+analyzes THIS binary and
  streams progress into the loading overlay). The common tail (health, keepalive,
  Program, load_functions) is shared, so domain.py and every view are untouched.
* _reconnect branches the same way: a dropped worker (segfault → closed socket)
  respawns a fresh WorkerClient — the connection-loss recovery already built
  works verbatim for the worker.
* launch.py adds --backend (env IDATUI_BACKEND); the worker path skips all the
  supervisor/server plumbing (it owns a private worker) and just sweeps stale
  locks + requires a binary. keepalive is a no-op for the worker (it never idles
  out). mcp remains the default — nothing changes unless you opt in.

Verified without idalib: all four files parse; --backend is in --help; IdaTui
constructs for both backends with both openers present; WorkerClient covers the
full client surface. The idalib E2E (experiments/worker_smoke.py, and actually
launching --backend worker) still can't run in this sandbox — it now reaps every
idalib spawn before a byte is written — but the mcp default is untouched and the
worker path reuses proven pieces (the unix protocol benched at ~50us/call; the
worker dispatches the same tool functions the HTTP path does).

To validate on a real box: ida-tui --backend worker targets/echo   (or run
experiments/worker_smoke.py for the headless read-path check).
</content>
</entry>
<entry>
<title>ux: fix truncated loading-splash message</title>
<updated>2026-07-23T22:28:04Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T22:28:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=cd25c9807d496728bcd8fc2687d3228d287cad03'/>
<id>urn:sha1:cd25c9807d496728bcd8fc2687d3228d287cad03</id>
<content type='text'>
The "auto-analyzing…" note crammed the "first open of a big binary can take a
while" caveat onto one line that overflowed the fixed-width splash box (it showed
truncated as "…first open of a big binary can"). Move the caveat to the splash's
static help line, keep the dynamic note short, and (in app.py, already committed)
widen the box to 72 and let the note/help wrap (height: auto) as a safety net.
</content>
</entry>
<entry>
<title>ux: label the startup wait as auto-analysis, not "starting a server"</title>
<updated>2026-07-23T21:58:59Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T21:58:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=2b7623ab730f486613f47dbcde7e5432ced24021'/>
<id>urn:sha1:2b7623ab730f486613f47dbcde7e5432ced24021</id>
<content type='text'>
The supervisor opens + auto-analyzes its seed binary BEFORE it binds the port
(idalib_supervisor.main), so the whole "starting analysis server" wait is really
IDA running initial auto-analysis. Relabel the overlay notes to say so:
"starting IDA — initial auto-analysis of &lt;bin&gt;…" then
"auto-analyzing &lt;bin&gt;… (Ns)  first open of a big binary can take a while".

Message-only change in _ensure_server (with the progress callback); no behavior
change. Verified the emitted notes with a mocked server-up poll.
</content>
</entry>
<entry>
<title>ux: start the analysis server from inside the TUI (no more dead window)</title>
<updated>2026-07-23T21:52:23Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T21:52:23Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=976fcb8440242d45565e19897f3ef1164a3c9940'/>
<id>urn:sha1:976fcb8440242d45565e19897f3ef1164a3c9940</id>
<content type='text'>
The remaining "long wait with no feedback" was the launcher's _ensure_server: it
started the supervisor and blocked ~10s polling for the port BEFORE the TUI (and
its overlay) existed. Only a one-line stderr message covered it.

Move that wait behind the overlay. The launcher now only does instant checks
(sweep stale locks if the server is down, honor --no-server) and hands the binary
straight to the TUI with ensure_server=True. The TUI's _connect starts the
server itself and narrates it in the loading overlay ("starting analysis
server… (Ns)") before opening/analyzing the binary. idb_open is idempotent, so an
already-open session is still adopted instantly (the launcher no longer needs to
probe sessions).

_ensure_server gained a progress callback so it reports to the overlay instead of
stderr (which would corrupt the TUI screen).

Verified with the server DOWN: chrome + overlay appear within ~0.5s, notes go
"starting analysis server… (0s)" -&gt; "opening echo — analyzing…" -&gt;
"echo — 128 functions…" -&gt; land on main. Pilot suite (db/ensure_server=False path)
green: startup/palette/view_toggle/disasm_nav/search/rename/follow_xrefs/
decomp_nav/mouse/listing_view/continuous_view/func_banners.
</content>
</entry>
</feed>
