aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
...
* ux: block with a "finding xrefs…" overlay while gathering xrefsblasty3 days1-6/+71
| | | | | | | | | | | | | | | | | | | | Gathering xrefs (xrefs_to + function_of over every site) runs in a worker, so the UI stayed live and a second 'x' (or other action) fired into a half-finished operation — duplicate requests, dialogs stacking, generally confusing behavior. Add a small blocking BusyScreen modal shown the instant 'x' is pressed: * re-entry guard (_xref_active) ignores a second 'x' while one gather is in flight, and the modal captures input anyway; * _present_xrefs dismisses it and opens the dialog when results are ready; * Esc cancels — a worker that finishes after the cancel won't pop a stale dialog (the _xref_active flag gates it); * every exit path clears it (no-subject case, and a try/except around the worker's slow calls) so it can't get stuck. Verified: 1st 'x' -> BusyScreen (xref_active=True); 2nd 'x' ignored (stack unchanged); results -> dialog + busy gone; Esc -> cancelled, no late dialog. follow_xrefs/xref_labels/decomp_nav/decomp_follow_self/view_toggle/disasm_nav/ search/mouse/listing_view/continuous_view/func_banners/startup green.
* nav: snap an xref-into-pseudocode jump to the line that holds the symbolblasty3 days1-5/+40
| | | | | | | | | | | | | | | | | Hex-Rays can attribute an address to a pseudocode line a step off from where the referenced symbol actually appears, so an xref jump could land on a line that doesn't contain the token (cursor at column 0, wrong line). Add _decomp_locate: anchor on the /*0xEA*/ marker line for the address, but when a token is known, snap to the whole-word occurrence NEAREST the anchor (preferring the anchor line, then the line just below). This fixes the marker/ symbol line discrepancy and disambiguates a symbol that appears multiple times by picking the occurrence closest to the address. The mid-function decomp-jump branch now uses it. Verified: real xref (sub_2060 from sub_20DD) still lands on 'sub_2060(a2);' cursor_x=4; synthetic discrepancy (marker on a line without the symbol) snaps to the nearest call-site line, not the far unrelated occurrence. Full suite green.
* nav: xref jump into pseudocode lands on the referenced symbol, not column 0blasty3 days1-1/+5
| | | | | | | | | | | | | | | | | The decompiler-jump column fix used fn.name (the target's containing function), which is correct when jumping to a function's own prototype but wrong for an xref jump to a call SITE: the token on that line is the symbol xrefs was invoked on, not the enclosing function. So x-pane jumps landed at column 0. Use focus_name (the xref's subject, already threaded through _goto_ea) as the token for a mid-function target; keep fn.name for the ea == fn.addr (prototype) case. Verified: xrefs to sub_2060 -> pick the caller site -> lands in sub_20DD's pseudocode on ' sub_2060(a2);' with cursor_x=4 = 'sub_2060'. Full suite (disasm_nav/view_toggle/search/follow_xrefs/xref_labels/mouse/decomp_nav/ decomp_follow_self/rename/rename_history/listing_view/continuous_view/ func_banners) green.
* nav: land the decompiler cursor on the function name, not column 0blasty3 days1-5/+11
| | | | | | | | | | | | | | | | | | Jumping to a function in the decompiler (follow/xref into pseudocode) parked the cursor at column 0 of the prototype line. Now it lands on the function's name in the prototype instead, mirroring the listing's ref-column behavior. _do_navigate's prefer_decomp branch: when the target is the function itself (ea == fn.addr) it uses the prototype line (0) and computes the name's column via _decomp_col_for; a mid-function target uses the ea-matched line (name column when present, else 0). The column rides through _open_decomp_entry -> NavEntry .dec_cursor_x -> _apply_decomp's view.show(cursor_x=...). Verified: following sub_2060 from pseudocode lands on line 0, cursor_x=28, which is exactly 'sub_2060' in "unsigned __int64 __fastcall sub_2060(int a1)". decomp_nav/decomp_follow_self/xref_labels/view_toggle/disasm_nav/search/mouse/ listing_view/continuous_view/func_banners/rename green (follow_xrefs is a pre-existing ordering flake; passes in isolation).
* ux: label the startup wait as auto-analysis, not "starting a server"blasty3 days1-3/+8
| | | | | | | | | | | 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 <bin>…" then "auto-analyzing <bin>… (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.
* ux: start the analysis server from inside the TUI (no more dead window)blasty3 days2-50/+46
| | | | | | | | | | | | | | | | | | | | | | | 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)" -> "opening echo — analyzing…" -> "echo — 128 functions…" -> 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.
* launch: sweep a crashed worker's stale DB locks before starting the supervisorblasty3 days1-0/+9
| | | | | | | | | | | | | | | | A binary whose worker was hard-killed mid-analysis leaves unpacked .id0/.id1/ .id2/.nam/.til files (no .i64). The supervisor is seeded with the target and opens it on startup, so a wedged DB there crash-loops the whole supervisor ("Failed to open initial binary: Remote end closed connection") and the launcher reports "supervisor exited during startup" -- before the TUI's own open-time recovery can ever run. When the launcher is about to start a fresh supervisor (server is down, so nothing can hold the DB), proactively sweep the target's stale unpacked lock files first (never the packed .i64). Safe because no server == no live session. Verified: after clearing bash's stale locks the supervisor starts and stays up 20/20 checks over 20s with no crash loop and a clean log.
* ux: show TUI chrome + a "loading…" overlay during binary open/analysisblasty3 days2-16/+104
| | | | | | | | | | | | | | | | | | | | | | | Two parts: 1) A LoadingScreen modal is pushed on mount and dismissed once we land on a function (or the symbol picker). Its note line mirrors _status, so you see "opening… analyzing", then "N functions…", instead of staring at empty panes. Esc hides it if you'd rather watch the status bar. 2) The real fix for the dead air BEFORE the TUI drew anything: the launcher used to block on idb_open (full analysis of a big binary) *before* starting the TUI, so the overlay only flashed after the wait was already over. Now the launcher only adopts an already-open session (instant); otherwise it defers the open to the TUI via open_path, and _connect does idb_open (with the crashed-worker lock-sweep + retry moved here) while the chrome + overlay are on screen. Added a ttl arg to IdaTui for the deferred open. Verified: overlay pushed on mount, present through open+function-load, dismissed on landing; TUI-driven open (open_path, db=None) opens echo and lands on main with notes "opening echo — analyzing…" -> "echo — 128 functions…"; startup/ palette/view_toggle/disasm_nav/search/rename/follow_xrefs/decomp_nav/mouse/ listing_view/continuous_view/func_banners green.
* fix: keep the pseudocode cursor/scroll when a rename re-decompilesblasty3 days1-1/+12
| | | | | | | | | | | | | | | | | | | | | Renaming a symbol in the decompiler re-decompiles and refreshes (good), but the pane rendered at the wrong scroll until the first cursor move nudged it back. Cause: _reload_active_code only cleared loaded_ea and re-showed, and dec_scroll_y isn't tracked on every move, so _apply_decomp fell into show()'s DERIVE branch -- a bare scroll_to that, on a freshly shown pane whose size isn't computed yet, clamps/leaves a stale frame until the next interaction. Now the decomp refresh snapshots the LIVE pseudocode cursor + scroll from the widget before forcing the recompile, so _apply_decomp takes show()'s robust _apply_scroll path (deferred re-apply + refresh(layout=True)) and repaints at the right place immediately. Verified on main (421-line pseudocode, 22-row viewport): before rename cursor=54/scroll=33; after rename cursor=54/scroll=33, cursor visible -- position preserved with no stale frame. rename/rename_history/decomp_nav/ decomp_follow_self/view_toggle/follow_xrefs/xref_labels/listing_view/ continuous_view/func_banners/search/mouse/disasm_nav green (follow_xrefs was an ordering flake; 14/0 in isolation).
* nav: land the cursor on the ref's column, not column 0blasty3 days1-6/+26
| | | | | | | | | | | | | | | | | When an xref/follow jump targets a line, the cursor now lands on the COLUMN of the referenced token (like search does) instead of the start of the line. The focus_name already threaded into _do_navigate (but unused) is now carried to _open_at -> _open_entry -> ListingView.load as a one-shot _pending_focus; once the row is loaded, _on_primed locates the token via _word_occurrences (after the opcode-column width is finalized) and sets cursor_x, then h-scrolls it into view. Also pass focus_name from the double-click follow path. When the token isn't on the destination line (e.g. jumping to a function entry), it's a no-op and the cursor stays at column 0. Verified: xref-jump to a 'call sub_2060' site lands cursor_x on 'sub_2060'; follow_xrefs/xref_labels/search/scroll_restore/disasm_nav/mouse/decomp_nav/ listing_view/continuous_view/func_banners/rename/view_toggle 70/0.
* fix: refresh-after-edit keeps the edited line on screen (rename now visible)blasty3 days1-2/+13
| | | | | | | | | | | | | | | | | | On a large binary the renamed label appeared not to update: the edit refresh reloaded the listing from a nav entry whose cursor/scroll could be stale, so on a huge segment the rebuilt model primed/scrolled to the wrong index and the renamed line landed off-screen (or in an unloaded region) -- looking like the rename never propagated. (Small binaries hid it because everything is near the top.) _reload_active_code now captures the LIVE listing position straight from the widget (the source of truth) instead of trusting nav-entry tracking, so the rebuilt model primes around the real cursor and the edited line stays put. Verified: with _cur.cursor deliberately corrupted to 0, renaming a label at row 56 keeps it on screen (0000210E DEEPLBL:); decomp->Tab->rename and pure-listing rename still stay on the label. rename/rename_history/comment/retype/view_toggle/ decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/disasm_nav/search/mouse/ listing_view/continuous_view/func_banners/scroll_restore 70/0.
* fix: Tab decomp->listing reuses the nav entry so edits stay putblasty3 days1-5/+25
| | | | | | | | | | | | | | | | | | | | | The previous fix reopened the listing via _goto_ea(push=False), which made _cur a DETACHED entry (not _nav[-1]). Cursor moves update _nav[-1], so _cur.cursor went stale: after moving to a label and renaming it, _reload_active_code -> _open_entry(cur) restored the stale cursor and scrolled the just-renamed label off-screen. The rename actually applied (model showed the new name) but the view jumped away, so it looked like it "didn't propagate". Now Tab from a jumped-to pseudocode view converts the CURRENT entry (which is _nav[-1]) to a listing view in place (_toggle_to_listing): set view="listing", position at the current line's address, and reopen. Cursor moves keep updating the same entry, so a subsequent rename reloads at the cursor and the renamed label stays on screen. Verified: decomp jump -> Tab -> move to loc_ label -> rename: cursor stays on the label, which now shows the new name (00002040 ZZLABEL:) in the viewport. view_toggle/decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/rename/ rename_history/disasm_nav/search/mouse/listing_view/continuous_view/func_banners/ scroll_restore 70/0.
* fix: Tab back from a decomp jump returns to the listing cleanlyblasty3 days1-2/+11
| | | | | | | | | | | | | | | | | | | | | | | | Regression from the decomp-jump feature: _open_decomp_entry clears _decomp_return, so Tab out of a jumped-to pseudocode view hit the fallback that just re-showed the (stale) listing widget WITHOUT reopening it and left _cur.view == "decomp". Two visible symptoms: * the listing showed the wrong/old function, and * a subsequent rename ran _reload_active_code -> _open_entry(cur), which saw view == "decomp" and bounced back into the decompiler; the rename also appeared not to propagate (you were looking at stale pseudocode). Fixes: * Tab from decomp with no F5 snapshot now opens the listing at the current pseudocode line's address (a real listing view, _cur.view = "listing"), so the correct function shows. * _reload_active_code forces _cur.view = "listing" when refreshing the listing, keeping the entry consistent with what's on screen. Verified: F5 -> follow ref in decomp -> Tab -> listing shows the TARGET function; renaming a label there stays in the listing and reflects the new name (RELABELED: and 'jnz RELABELED'). view_toggle/decomp_nav/decomp_follow_self/ follow_xrefs/xref_labels/rename/rename_history/disasm_nav/search/mouse/ listing_view/continuous_view/func_banners 70/0.
* nav: jumping from the decompiler stays in the decompilerblasty3 days1-8/+61
| | | | | | | | | | | | | | | | | | | | Following a reference or picking an xref target from the pseudocode dropped you back into the linear listing. Now such jumps stay in the decompiler when the target is a decompilable function, landing on the pseudocode line that matches the target address; non-decompilable targets still fall back to the listing. * NavEntry gains a `view` field ("listing"/"decomp"); _open_entry restores an entry in whichever view it was seen in (so back/forward also keep pseudocode). * _do_navigate/_goto_ea take prefer_decomp; when set and the target function decompiles, _open_decomp_entry opens it in the decompiler as a real nav-history push (snapshotting the source pseudocode position so 'back' returns to it). * _follow_decomp and _on_xref_chosen pass prefer_decomp when the source view is the decompiler. Verified: follow a ref from decomp -> lands in the target's pseudocode; 'back' returns to the source pseudocode; listing follow still lands in the listing. decomp_nav/decomp_follow_self/follow_xrefs/xref_labels/view_toggle/disasm_nav/ search/mouse/listing_view/func_banners/continuous_view 62/0.
* listing: on-screen jumps just move the cursor, don't re-scrollblasty3 days1-4/+10
| | | | | | | | | | | | Extend the jump-context behaviour: if the target is already visible in the current viewport (same listing model, cursor index within [top, top+height)), leave the scroll offset untouched and only move the cursor to the target. Only off-screen jumps re-scroll (target parked _JUMP_CONTEXT lines below the top). Verified: an in-viewport jump keeps scroll_top constant while moving the cursor onto the target; off-screen jumps still get the 4-line context margin. scroll_restore/disasm_nav/follow_xrefs/xref_labels/mouse/search/listing_view/ continuous_view/func_banners 54/0.
* listing: keep a few lines of context above a jump targetblasty3 days1-0/+6
| | | | | | | | | | | | | | | Jumping (goto/follow/xref/open) parked the target's first line on the very top row of the viewport, which is cramped. Now a fresh navigation scrolls so the target sits _JUMP_CONTEXT (4) lines down, leaving surrounding context above it; the cursor still lands on the target's first instruction. Only applies when the scroll is being derived (NavEntry.scroll_y == -1). Back/ forward restores carry their own saved scroll_y, so they still return to the exact previous viewport. Verified: jumps to sub_3720/sub_2060/main land the cursor on the entry with a 4-line margin above; scroll_restore/disasm_nav/follow_xrefs/mouse/search/ listing_view/continuous_view/func_banners 51/0.
* fix: follow/double-click a label lands on the label, not the function entryblasty3 days2-2/+33
| | | | | | | | | | | | | | | | | resolve(name) went through lookup_funcs, which for a mid-function label (loc_/locret_) does get_name_ea -> label ea, then get_function(ea) and reports the CONTAINING function's entry address. So double-clicking (or following) a label jumped to the top of the function instead of the label's own address. Add a server tool resolve_names that returns the address a name actually denotes via idaapi.get_name_ea (functions, labels and data alike), and route Program.resolve() through it, keeping lookup_funcs only as a fallback for the "did you mean ..." suggestion on an unknown name. Verified: resolve('loc_2040') -> 0x2040 (was the function entry); double-click follow on a label row lands on the label ea. Regression sweep follow_xrefs/xref_labels/rename/rename_history/decomp_follow_self/search/ listing_view/disasm_nav/mouse 50/0. Needs a supervisor restart (new tool).
* fix: don't let a late navigation steal focus from an open promptblasty3 days2-4/+28
| | | | | | | | | | | | | | | | | | | | | | | | Real, data-loss-capable bug: opening a function that is already the current one (e.g. after scrolling away from it) schedules an async re-navigation. If the user opens the search prompt ('/') before that navigation lands, the navigation's _show_active() called lst.focus() and yanked focus OUT of the search box. The next typed characters then routed to the listing as verbs -- and 'u' = undefine, silently deleting a function. (This is what produced the flaky "search finds nothing" + "no function for <ea>" cascade in the pilot.) Fix: _show_active() no longer grabs focus for the code view while any prompt overlay (search/rename/comment/retype/goto/func-filter) is visible -- those own the keyboard until dismissed. View visibility is unchanged; only the focus grab is suppressed. Also harden the pilot's open(): wait for the listing cursor to actually LAND on the target address, not merely for _cur.ea to match (which is stale-true when re-opening the same function), so tests can't proceed on a not-yet-moved cursor. Verified: the exact failing order disasm_nav->search->follow_xrefs is green; broad focus-sensitive sweep (startup/disasm_nav/view_toggle/hex/search/rename/ comment_func/retype/follow_xrefs/decomp_nav/mouse/listing*/continuous_view/ func_banners/region_define) 89/0.
* listing: restore full-segment search load (find every match)blasty3 days1-5/+29
| | | | | | | | | | | | | | | | | | Revert the "search only the loaded portion" shortcut from f5aaf50 and go back to a single, guarded background load_all before matching, so a search finds every occurrence in the segment (not just whatever streamed in so far). The guard (_search_loading + queued callbacks) shares ONE load across per-keystroke updates instead of spawning/cancelling a worker each keypress. The wedge that shortcut was meant to avoid turned out to be unrelated to the search load: it is a pre-existing navigation bug (opening a function right after scrolling to the bottom of another leaves the listing cursor out of range and '/' fails to focus the search input, so typed chars leak into the view as listing verbs, one of which is 'u'=undefine). Reproduces at 0c097f5, before any of this work; tracked separately. Verified in isolation: search 3.1s, matches found; search/mouse/func_banners/ listing_view/continuous_view/decomp_nav/follow_xrefs 45/0.
* views: highlight all occurrences of the token under the cursorblasty3 days1-1/+51
| | | | | | | | | | | | | | | | | | | Editor-style highlight-all: whenever the cursor lands on an identifier, every other occurrence of that same whole-word token is highlighted across the visible listing AND the decompiler view (IDA's identifier-highlight behaviour). * new _word_occurrences() finds whole-word spans in a line (reuses the same char==cell alignment search already relies on). * ColumnCursor gains _hl_word + _refresh_hl(): after any cursor move (h/l, w/b, 0/eol, j/k, mouse click, search jump) it recomputes the token under the cursor and repaints the viewport only when it changed. Tokens must be >=2 chars and start with a letter/underscore. * ListingView.render_line and DecompView.render_line overlay every occurrence with the existing _S_WORD background; the block cursor still marks the current. Verified: cursor on 'rbp' highlights 5 visible listing rows; cursor on 'v23' highlights it across the decompiler; mouse/disasm_nav/decomp_nav/listing_view/ func_banners/follow_xrefs/continuous_view/decomp_follow_self 42/0.
* listing: code labels on their own line; search loaded portion (no wedge)blasty3 days3-19/+24
| | | | | | | | | | | | | | | | | Code labels (loc_XXX/jump targets) get their OWN line at depth 0, like IDA, instead of inline with the instruction: the heads walker (annotate) emits a kind=label row ('loc_XXX:') before a named non-function-start code head and strips the name from the instruction. ListingModel doesn't index label rows (goto/xref/follow still land on the code); ListingView renders them at depth 0. Also fix a search regression the extra rows exposed: search no longer forces a blocking full-segment load_all (which stalled/thrashed the worker and wedged the session on the larger annotated listing). It searches the loaded portion; the background grower streams the rest and unloaded lines are skipped until they arrive. Verified: labels on their own line; search 2.9s (was 51.9s+wedge); search/mouse/ rename_history/continuous_view/func_banners/follow_xrefs 26/0.
* listing: two-level indent, address on headers, drop 'near', opcode cap+cycleblasty3 days3-27/+53
| | | | | | | | | | | | | | | Polish per feedback: proc header drops the meaningless 'near' ('name proc'); the function-name/proc header now shows the address like every line; two-level indent (function names at depth 0, opcode bytes + instruction text one level deeper, matching IDA's label/instruction columns); 'o' now CYCLES the opcode column off->limited->full and is shown in the footer; 'limited' mode caps long runs at the first 8 bytes + ellipsis so 15-byte x86-64 insns don't blow out the column (width capped to match). _line_plain/render_line share the layout so cursor/search stay aligned. Verified func_banners+disasm_nav/mouse/region_define/listing_view/ listing_struct_expand/listing_name_addr/search/continuous_view green. Needs a supervisor restart.
* listing: IDA-style function boundary banners in the unified viewblasty3 days4-3/+77
| | | | | | | | | | | | | | | | | Each function gets clear boundary annotations in the continuous listing: a blank + '; ==== S U B R O U T I N E ====' separator and a 'name proc near' header before the entry, and a 'name endp' + rule after the last item. The function name moves to the proc header (stripped from the inline entry instruction). Server: heads gains an annotate flag (default off, so DisasmModel/test_domain stay 1:1 with instructions); when on, _rows_for emits kind=sep/funchdr rows at function starts/ends. ListingModel requests annotate=true and does NOT index banner rows by ea, so goto/xref/follow land on real code. ListingView renders sep (dim) and funchdr (bold gold); new _S_SEP/_S_FUNCHDR styles. Verified: func_banners 5/0; region_define/listing_view/listing_name_addr/ listing_struct_expand/continuous_view/follow_xrefs/disasm_nav/scroll_restore/ paging green. Needs a supervisor restart (heads tool changed).
* unify: the continuous listing is the one code view; deprecate DisasmViewblasty3 days4-286/+227
| | | | | | | | | | | | | | | | | | | | Full IDA-style unification. The function-bounded DisasmView is gone from the UI: navigation opens ONE continuous segment listing (functions+data+undefined interleaved) at the target; F5/Tab decompiles the function under the cursor and back. The listing gained the opcode column (Head.raw, 'o' toggle) so rendering matches disasm. Routing: _do_navigate/_open_function/_open_entry target the listing; _show_active drops disasm; _active in {listing,decomp,hex}; _pref=listing. Decomp is a per-function toggle with a listing fallback on failure. Edits reload in place (_reload_active_code); bump_names() drops the listing cache so renames refresh. Listing 'n' is symbol-aware. ListingModel gains cached_line/lines shims; Head.label. DisasmView removed from compose/handlers; rpc updated. DisasmModel kept (domain/test_domain). Tests migrated (c.dis->ListingView; open(decomp) F5s; xref/search re-pointed). Per-scenario green across view_toggle/rename/follow_xrefs/xref_labels/decomp_*/ search/mouse/startup/continuous_view (30/1, 1 cold-decompile flake).
* listing: F5/Tab decompiles the function under the cursor (IDA-style) — step 2blasty3 days2-9/+76
| | | | | | | | | | | In the continuous listing, F5 (or Tab) decompiles the defined function the cursor is inside; F5/Tab again returns to the linear listing exactly where you left. Not-in-a-function -> a message; decompile failure falls back to the listing (not the bounded disasm). Listing position is snapshotted so the round-trip is lossless. Verified: continuous_view F5 round-trip + view_toggle/disasm_nav/follow_xrefs/ region_define/listing_view 45/0.
* listing: render opcode bytes (shared engine with disasm) — M4/UX step 1blasty3 days3-9/+96
| | | | | | | | | | | | | | | The flat listing was a subset of the disasm view (no opcodes). Now the listing carries an opcode-bytes column with the same format + o toggle: Head gains a raw field, ListingModel fills it for code heads via one bulk read per page (bounded, variable-length safe) + tracks the widest opcode; ListingView renders/pads the column, settling width as pages stream in. render_line/_line_plain share the addr+opcodes+label+mnem/text layout with DisasmView. Test harness all_funcs() reloads the index if a prior bump_items() cleared it (was crashing biggest() with max([])). Verified: code heads carry raw (max_raw_len 11); listing suite + continuous_view opcode-parity 26/0.
* app: 'L' opens the continuous segment listing (functions+data interleaved)blasty3 days2-0/+65
| | | | | | | | | | | | Function views are bounded to one function; 'L' from any code view opens the whole-segment ListingView at the cursor instead -- one long flat listing where functions/data/undefined interleave and you scroll straight from one function into the next. Reuses the region listing path (_goto_continuous). New continuous_view scenario: open a function, press L, assert continuous listing opens, cursor at the origin function, listing spans past the function bounds, code interleaves with data. 22/0 with disasm_nav/view_toggle/ region_define (L is additive).
* unify: back the function disasm view on the heads listing walker (M4 item 4)blasty4 days2-13/+29
| | | | | | | | | | | | | | | The function disasm view is now a filtered listing: DisasmModel sources its lines from the heads walker bounded to [func.start, func.end) instead of the disasm tool, so both code views render from the one listing mechanism. DisasmView UI (opcode bytes, o-toggle, Tab, rename) preserved -> no test churn. total() stays on disasm include_total: paging heads for the count hit the MCP response-size limit (count=1000 truncated -> wrong count) and corrupted prime. include_total is one fast exact call; for a code function it equals the heads row count backing the lines. _fetch_block uses count=257 (safe). Verified: disasm-heavy scenarios 56/0; full suite 127/1 (1 = flaky filter). M4 complete.
* listing: expand struct-typed globals into member rows (M4 item 3)blasty4 days3-5/+94
| | | | | | | | | | | A struct-typed data item rendered as one dense 'MyS <...>' line. Now the heads walker emits indented member rows after the summary (from get_udt_details): +off name type, each a head of kind member at ea+offset. ListingView renders them indented/dimmed; _line_plain matches for cursor/search. Verified: a struct global expands to +0 a int / +4 b char[4] / +8 c __int16, member rows carry field addresses. New listing_struct_expand scenario; listing suite 22/0 (in-process).
* TODO: M4 progress — string auto-detect + streaming done; ↵blasty4 days1-3/+11
| | | | struct-expand/unify deferred
* perf: stream the listing incrementally instead of load_all on open (M4)blasty4 days2-5/+60
| | | | | | | | | | | | | | Opening a listing walked the WHOLE segment (load_all) before showing anything -- a blank pane for seconds on a big .text. Now _prime loads just the viewport around the cursor (appears instantly) and _grow streams the rest in the background, growing virtual_size as pages land (throttled). Search finishes loading first (needs the whole segment). ListingModel gains a _load_lock so the grower and an in-view search can both drive page loads without double-fetching; new public load_next_page. Verified: ensure(1100)->partial(1500,incomplete), load_all->full(5036,complete); 3 concurrent loaders -> 0 duplicate heads; UI listing+search 30/0 (in-process).
* app: 'a' — make string literal in the listing (M4 richness)blasty4 days4-1/+102
| | | | | | | | | | | | IDA's 'A' key. New make_string server tool (ida_bytes.create_strlit, auto-length to the terminator, undefines items in the way first, returns size + decoded contents). Program.make_string wraps it; 'a' on a listing head routes through the existing edit plumbing (EditItemRequested kind=string -> _do_edit_item -> make_string -> bump_items -> reopen). Verified: make_string at a .rodata addr creates the literal and IDA auto-names it (aUsage for 'usage'). New listing_make_string scenario drives it through the UI; listing suite 15/0. Needs a supervisor restart.
* perf: derive segment map from file_regions, not survey_binary (hex open ~3400x)blasty4 days2-25/+47
| | | | | | | | | | | | | | | | | | The slow hex load wasn't the byte reads (read_raw fixed those) -- it was opening the pane at all: hex_model -> image_range -> sections called survey_binary, which computes function counts/strings/stats and takes ~24s on libcrypto, blocking the first hex open (and section_of/segment_bounds/ region_label in the listing). sections/file_regions/image_range now share one cheap _segments source backed by the injected file_regions tool (a plain segment walk, ~7ms), which gains a name field so it fully replaces survey_binary for the segment map. Falls back to survey_binary only if the tool is missing. Measured (libcrypto): segment map 24240ms -> 7.1ms (~3400x); hex open goes from ~24s to ~11ms. Correctness verified (names, section_of/segment_bounds/ file_offset); hex+listing scenarios 24/0, full suite 123/1 flaky. Needs a supervisor restart.
* app: listing 'n' names the address (can name a bare/undefined byte)blasty4 days2-0/+123
| | | | | | | | | | | | | | | | Repro: shrink a u16 data field to u8 in the listing; the freed byte at addr+1 becomes undefined, but you couldn't name it. 'n' posted RenameRequested with word_under_cursor(), and _do_rename treats that as an existing symbol to rename -- a 'db ?' line has no symbol, so it failed. In the ListingView, 'n' now names the ADDRESS under the cursor: opens the name prompt prefilled with the head's current name (empty for an unnamed byte) and applies it via rename {data:{addr,new}} (the server's address form of the data rename, which set_name's the ea even when nothing is defined there), then bump_items + reopen so the label shows. Code-view symbol rename is unchanged. Server already supported it; new listing_name_addr scenario reproduces the full workflow. Full suite 123 pass / 1 pre-existing flaky (filter).
* app: on startup, land on main() (else pop the symbol picker)blasty4 days2-3/+70
| | | | | | | | | | | | | | | Instead of staring at an empty pane after the function list loads, jump somewhere useful: once the index is complete and nothing's open yet, open main() (tries main/_main/wmain/WinMain/wWinMain in order); if there's no entry function, pop the fuzzy symbol picker so you can pick one. Fires exactly once (guarded by _cur + _did_auto_land) and never steals focus from a user who already navigated during load. Tests: new auto_land scenario covers both branches (jump-to-main when present, picker fallback otherwise) + the idempotency guard. Boot/startup now key "load complete" off _func_index.complete instead of the status string (auto-land legitimately overwrites the "N functions" status with the landed fn). Full suite 120 pass / 1 pre-existing flaky (filter).
* fix: crash on an unnamed function (None name) in the symbol paletteblasty4 days2-1/+10
| | | | | | | | | | | | | | | | `Ctrl+N` then typing crashed with `AttributeError: 'NoneType' object has no attribute 'lower'` when a function had no name: Func.from_raw took `d["name"]` verbatim, so a server-returned null/missing name became None and blew up _fuzzy (and would break sort/rename-prefill too). Two-layer fix: * Func.from_raw synthesizes IDA's `sub_<ADDR>` for a null/empty/missing name (and tolerates a missing size) so `name` is always a str for every consumer; and * _fuzzy guards against a falsy name defensively. Verified: Func.from_raw({addr, name:null}) -> "sub_1000"; _fuzzy(None,'m') -> None (no raise). Pilot palette + startup 9/0.
* perf: read_raw tool — bulk byte reads for the hex view (5–8x)blasty4 days2-2/+62
| | | | | | | | | | | | | | | | | | | | | | The hex view's lazy-load was dominated by the byte-read path, which was slow on two axes: * server: get_bytes uses read_bytes_bss_safe, a per-byte loop (is_loaded + get_byte = 2 IDA calls/byte → ~8k calls for a 4KB block), and encodes the result as "0x00 0x01 ..." text (~5x wire bloat); * client: read_bytes re-parsed that with int(tok,16) per byte; * and get_bytes silently TRUNCATES ≥16KB responses to "0x..." (wrong data). New injected `read_raw` tool does a single bulk ida_bytes.get_bytes (C-speed) and only re-checks is_loaded for the sparse 0xFF bss-sentinel bytes, returning one contiguous hex string. Client decodes with bytes.fromhex (C-speed). domain read_bytes uses it with a transparent fallback to get_bytes on older servers (cached _no_read_raw flag). HEX_BLOCK 4096→16384 now that big blocks are cheap and no longer truncate → 4x fewer round-trips while scrolling. Measured (echo, warm): 4KB 22.7ms→4.7ms (4.9x), 8KB 51.6ms→6.6ms (7.8x), and 16KB works (15.7ms) where get_bytes truncates outright. Byte-exact match vs the old path; disasm opcode bytes (also read_bytes) benefit too. Pilot hex + disasm_nav 12/0. Needs a supervisor restart (new server tool).
* launch: `ida-tui foo.elf` one-shot wrapper (server + locks + session)blasty4 days5-0/+280
| | | | | | | | | | | | | | | | | | | | | | | | | A caveman entry point so you don't hand-craft the plumbing every time. It: * ensures the ida-pro-mcp supervisor is up — starts spawn.sh detached (start_new_session, tmux-free) and waits for the port if it's down; * recovers a binary wedged by a hard-killed worker — sweeps the stale unpacked .id0/.id1/.id2/.nam/.til next to the .i64 and retries (the packed .i64 is never touched); * adopts an already-open session for the same binary (idempotent), else idb_opens it with a sane idle-TTL; * launches the TUI attached to that session with keepalive on. ./ida-tui /path/to/binary # open a binary and drive it ./ida-tui # attach to the sole session ./ida-tui --db <id> # attach to a specific session Pieces: idatui/launch.py (logic, reuses pane.py's server probe), a repo-root `ida-tui` sh wrapper (resolves ~/ida-venv python, keeps idatui importable from any cwd), and an `ida-tui` console-script in pyproject. --help works without textual (app imported late). README documents both the one-liner and the manual recovery. gitignore bin/ (binary targets, like targets/). Verified live: ensure_server up-detection, open, adopt (same session id), and lock-sweep all work against a running supervisor.
* app: 'd' — define typed data in the listing (make_data, M3)blasty4 days4-3/+144
| | | | | | | | | | | | | | Press 'd' on a listing head to define typed data at that address via a prompt (the ".data type definitions" backlog item). Mirrors the retype prompt flow: MakeDataRequested -> #makedata Input (prefilled with a size-appropriate default type: unsigned __int8/16/32/64 or char[N]) -> _do_make_data worker -> Program.make_data -> bump_items -> reopen the listing in place. Accepts any C type IDA's SetType understands: int, char[16], my_struct, T *arr[4], ... Pilot: listing_view gains a deterministic sub-test — undefine a data head to synthesize an unknown run, assert it renders as `unknown`, then 'd' char[4] over it and assert it becomes a `data` head. 118 pass / 1 pre-existing flaky (filter); rpc_smoke 29/0.
* server: coalesce undefined byte runs in the heads walkerblasty4 days1-6/+41
| | | | | | | | | | | | | | | | The M2 fix (step by get_item_end so undefined bytes render and arbitrary addresses land exactly) emits one row per undefined byte, which would explode a large .bss/undefined region into millions of rows and make load_all crawl. Collapse a run of consecutive undefined bytes into a single `db N dup(?)` row (its end found in O(1) via next_head, which skips undefined). A single stray undefined byte still renders normally (shows its value), so exact-address navigation and the c/p/u workflow are unchanged: a 42-byte gap is one row at the run start, and 'p' there auto-analyzes the function. Verified: undefining a function yields one `db 42 dup(?)` row (not 42); echo .text stays 5036 heads; data segments unaffected. region_define + listing_view + test_domain[listing] all green. Needs a supervisor restart.
* app: ListingView — virtualized flat code+data listing for regions (M2)blasty4 days5-37/+378
| | | | | | | | | | | | | | | | | | | | | | | | | | | The real disassembly-listing view. ListingView is a line-virtualized ScrollView (reusing ColumnCursor/SearchMixin/NavMixin) backed by ListingModel, rendering code, data (db/dw/dd, strings, jump tables) and undefined bytes interleaved with per-kind styling. Non-function regions now open in it instead of the M0 DisasmModel stopgap. Integration (IdaTui): a new `listing` active mode. _open_entry routes region entries to ListingView; _show_active/compose add it; follow/xrefs/comment and the c/p/u edit verbs handle it (define_func upgrades a region straight to the function view); backslash reaches hex and returns via _code_mode (so leaving hex from a region lands back on the listing, not a func view); Tab explains there's no pseudocode for a region. rpc._active_widget learns `listing`. Server fix (the important one): the `heads` walker now steps by get_item_end instead of next_head. next_head SKIPS undefined bytes, so after undefining a function its start address wasn't even a listing line and the cursor snapped to the next defined item; stepping by item-end renders undefined bytes as `db ?` lines (IDA-accurate) and makes navigation land exactly on any address — essential for "go to unmarked bytes and hit c". Needs a supervisor restart. Tests: region_define rewritten to assert the ListingView path + segment-wide listing + p-upgrade; new listing_view scenario (data-segment listing renders data heads, cursor reports head ea, backslash->hex->back). Pilot 115 pass / 1 pre-existing flaky (filter); rpc_smoke 29/0; test_domain 27/0.
* server+domain: heads walker + ListingModel — flat code/data listing (M1)blasty4 days3-1/+299
| | | | | | | | | | | | | | | | | | | | | | | | | | | The keystone for a real disassembly-listing view (unlike DisasmModel, which is one function, code-only). Two pieces: server/patch_server.py: inject a `heads` tool. It walks item heads over a segment with next_head/prev_head and renders each via generate_disasm_line, so it returns a flat listing where code, data (db/dw/dd, strings, jump tables) and undefined bytes all appear as typed rows {ea,kind,size,text,name}. Unlike `disasm` (code-only, bails at the first data byte) it shows the whole segment. Address-paged: chain forward via cursor.next, page up with back=true (returns the N heads ending before addr, in forward order, + cursor.prev). domain.py: ListingModel — a lazily-grown, segment-scoped head index (FunctionIndex-style forward paging via cursor.next; line index == position in the walked list). ensure_ea() gives random access to an address (resolving a mid-item byte to its containing head). New Head dataclass, Program.listing() (cached per segment) + segment_bounds(); section_of() now derives from it; bump_items() clears the listing cache too. Verified live: heads renders strings/jump-tables/unknown correctly, forward chaining + back-paging work; ListingModel walks echo .text (5036 heads, ~276ms) with cached windows and mid-item address resolution. tests/test_domain gains a [listing] section (27 passed). Pilot hex/region_define/disasm_nav green. Note: adding a server tool needs a supervisor restart (workers respawn).
* app: non-function regions — open a flat listing + c/p/u edit verbs (M0)blasty4 days4-6/+200
| | | | | | | | | | | | | | | | | | | | | | | | | Navigating to an address not inside a function no longer refuses ("no function contains X"): _do_navigate now opens a region view (flat listing anchored at the EA, forced to disasm since there's no pseudocode). The server's no-function disasm already walks heads to the segment end, so DisasmModel is reused as-is; NavEntry gains is_region. Adds the IDA c/p/u structure-edit verbs on the disasm view: c = define_code (undefine-first, since create_insn won't carve a live item) p = define_func u = undefine wired via EditItemRequested -> _do_edit_item worker -> Program.define_code/ define_func/undefine. define_func upgrades the region to a real function view in place. New Program.bump_items() invalidates the item/function structure caches (disasm blocks, decomp, function indices + name gen) — broader than bump_names(), which only covers renames. Pilot: new `region_define` scenario undefines a small function, navigates to the bare region via the 'g' prompt (asserts it opens, not refused), then 'p' recreates the function and upgrades the view; restores the IDB in a finally. 107 passed / 1 pre-existing flaky (filter, fails on baseline).
* pane: reap leaked idalib workers; drive: friendlier name resolution (fixes ↵user2026-07-123-6/+143
| | | | spawn-hang + bare KeyError)
* TODO: opcodes in disas view doneblasty2026-07-111-1/+1
|
* disasm: render opcode bytes (toggle 'o'), padded to widest insnblasty2026-07-118-10/+253
| | | | | | | | | | | | | | Fetch per-instruction opcode bytes alongside the disasm listing and show them in a column between the address and the mnemonic. Instruction length comes from consecutive addresses (variable-length safe: x86 movabs shows its full 10 bytes); the block over-fetches one instruction for the last line's boundary, falling back to the function end for the final insn. The column pads to the widest instruction across the whole function: _fetch_block tracks a running max, and on load a background scan_bytes() settles a stable width so padding doesn't jump as blocks stream in. _op_field is shared by the rendered strip, _line_plain, and search _fmt so cursor/match offsets stay aligned. 'o' toggles the column.
* docs: add READMEblasty2026-07-111-0/+97
|
* spawn.sh: run location-independent + env-configurable host/port/targetuser2026-07-111-3/+15
|
* drive: make `dis` drive the live UI too (generalize _show_decomp -> _show_view)user2026-07-111-7/+11
|
* fix: don't hang drive pc on undecompilable functionsuser2026-07-113-4/+55
| | | | | | | | | | | | | | | | toggle_view's settle predicate (lambda: app._active != before) never fired when tabbing toward pseudocode on a function Hex-Rays can't decompile: App._apply_decomp snaps the view back to disasm, so _active returns to its prior value -> full 20s settle timeout (x2 in _show_decomp, ~40s for drive pc). Recognize the decomp-failed fallback as settled. Also harden two amplifiers surfaced by the same case: - rpcclient: the CLI socket had no read timeout and would block forever on any server slowness; add a bounded settimeout (IDATUI_RPC_TIMEOUT, default 90s) with a clear error. - domain.decompile: pass a bounded 15s timeout and cache failures, so a failing decompile can't sit at the 30s client default or be re-run by transport retries.