summaryrefslogtreecommitdiffstats
path: root/idatui/app.py (follow)
Commit message (Collapse)AuthorAge
* fix: keep the pseudocode cursor/scroll when a rename re-decompilesblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | | 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 0blasty2026-07-23
| | | | | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | | | | | | | 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 putblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | | 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 cleanlyblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | | | | | 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 decompilerblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | 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-scrollblasty2026-07-23
| | | | | | | | | | | | 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 targetblasty2026-07-23
| | | | | | | | | | | | | | | 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: don't let a late navigation steal focus from an open promptblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | | | | | | | 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 cursorblasty2026-07-23
| | | | | | | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | | | | | | 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+cycleblasty2026-07-23
| | | | | | | | | | | | | | | 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 viewblasty2026-07-23
| | | | | | | | | | | | | | | | | 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 DisasmViewblasty2026-07-23
| | | | | | | | | | | | | | | | | | | | 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 2blasty2026-07-23
| | | | | | | | | | | 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 1blasty2026-07-23
| | | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | 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).
* listing: expand struct-typed globals into member rows (M4 item 3)blasty2026-07-23
| | | | | | | | | | | 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).
* perf: stream the listing incrementally instead of load_all on open (M4)blasty2026-07-23
| | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | 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.
* app: listing 'n' names the address (can name a bare/undefined byte)blasty2026-07-23
| | | | | | | | | | | | | | | | 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)blasty2026-07-23
| | | | | | | | | | | | | | | 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 paletteblasty2026-07-23
| | | | | | | | | | | | | | | | `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.
* app: 'd' — define typed data in the listing (make_data, M3)blasty2026-07-22
| | | | | | | | | | | | | | 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.
* app: ListingView — virtualized flat code+data listing for regions (M2)blasty2026-07-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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.
* app: non-function regions — open a flat listing + c/p/u edit verbs (M0)blasty2026-07-22
| | | | | | | | | | | | | | | | | | | | | | | | | 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).
* disasm: render opcode bytes (toggle 'o'), padded to widest insnblasty2026-07-11
| | | | | | | | | | | | | | 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.
* app: multi-line comments via literal \\n (long notes were clipping)blasty2026-07-10
| | | | | | | | | The comment prompt is single-line, so a long note rendered as one giant '//' line that ran off the right edge and got clipped. Interpret a literal '\\n' (backslash-n) in the comment text as a real newline in _do_comment (the single choke point for the ';' prompt and the RPC 'comment' verb) — Hex-Rays renders each as its own '//' line. Verified live and locked in the comment_func scenario (multi-line render); suite 104 green.
* app: comment on a no-address line -> function comment (was refused)blasty2026-07-10
| | | | | | | | | | Pressing ';' on the decompiler's signature or local-declaration lines did nothing ('no address on this line to comment') because those lines carry no /*0xEA*/ marker. Fall back to the current function's entry ea so commenting the header annotates the function itself; the prompt says 'function comment @ ...'. Body-line comments are unchanged. New scenario comment_func locks it (suite 104 green).
* app: drop the Header (keep the Footer); rpc: function-scope xrefs_fromblasty2026-07-10
| | | | | | | | | | | | | | The clock/title Header added noise (and made screen() non-deterministic); remove it from the app itself — the Footer stays. Layout shifts up a row; scenario suite still 101 green. xrefs_from on a function was near-useless: the server's xrefs_from is address-scoped, so passing the entry ea only returned the fall-through from the first instruction. For a function target it now returns whole-body references from the decompiler (callees + string/data refs: {to,name,string,is_func,type}); an explicit 0xADDR stays address-scoped. Verified live: xrefs_from main now lists setlocale/getopt_long/sub_* etc. rpc_smoke: 25 green.
* rpc: unix-socket puppeteering server (raw keys + introspection + screen)blasty2026-07-10
| | | | | | | | | | | | | | | Run the TUI with --rpc <sock> and it renders normally while an asyncio listener on the same loop lets another process drive it. Handlers touch the UI directly (same loop, no thread hop), so every injected key has the identical on-screen effect a keyboard would — the point for livestreaming. Newline-delimited JSON. v1 methods: keys/text (raw injection, via the same _press_keys the pilot uses; text can interleave wait:<ms> for a typed-out look), state/view/screen/functions (introspection; screen() is a full plain-text render of exactly what the viewer sees). No auth by design — the socket is 0600, local only. tests/rpc_smoke.py drives it end-to-end over a real socket (7 green).
* hex view: file offsets + 'g' goto (dedicated input)blasty2026-07-10
| | | | | | | | | | | | | Add a file_regions server tool (get_fileregion_offset per segment) and Program.file_regions/file_offset so a virtual address maps to its raw on-disk offset without a format-specific header parser. The hex view now shows a VA column and a file-offset column side by side, and the status line reports file+off. Goto moves to its own top-level #goto Input instead of overloading the function-filter box (which is hidden with the names pane, so goto was unreachable there). In the hex view 'g' jumps the cursor to an address (bounds-checked against the image); elsewhere it navigates as before.
* hex view: raw image bytes (VA-addressed), synced to the code cursorblasty2026-07-10
| | | | | | | | | | | | | | | | New HexView: a line-virtualized 16-bytes/row dump of the whole loaded image, addressed by virtual address. Format-agnostic — the range/segments come from IDA (image_range over sections()), not any file header; gaps read back as zeros. Backslash toggles it, synced to the address under the disasm/pseudocode cursor (see the naked bytes of some code/data). In hex: hjkl/paging navigate a byte cursor (status shows the section), Enter jumps the code view to the byte, Tab/Esc/backslash return to the preferred code view. A third _active state; block -cached + prefetched like the disasm model. domain: HexModel + Program.hex_model/read_bytes/image_range (get_bytes regions). Fixed a self-deadlock (hex_model held _lock while sections() re-locked). Pilot: sync, actual bytes, byte-step, return. full suite 98/98.
* retype: set variable/function types with 'y' (IDA-style), via structured toolsblasty2026-07-10
| | | | | | | | | | | | | | | | Instead of parsing pseudocode text, add structured server tools (server/ patch_server.py, alongside del_type): - func_types(addr): prototype + local variables (name/type/is_arg) - set_lvar_type(addr,var,type): retype a decompiler local, working on auto/ register vars too (stock set_type only updates already-user-modified lvars) 'y' in a code view retypes what's under the cursor: a local variable (prompt prefilled with its current type) or a function (prompt prefilled with its full prototype). Applies via set_lvar_type / set_type, then recompiles + refreshes. Copy-line moved off 'y' to Ctrl+Y. domain: Program.func_types / set_function_type / set_lvar_type (LVar/FuncTypes). Pilot: function-prototype retype (prefill + apply). full suite 94/94.
* struct editor: auto-format the definition on saveblasty2026-07-10
| | | | | | | | After a successful declare, re-fetch the canonical struct_source and put it back in the editor, so the normalized layout/types show immediately instead of only after re-selecting the struct from the list (e.g. a pasted one-liner becomes multi-line, 'unsigned long' -> 'unsigned __int64'). _loaded_src is updated to the formatted text so it isn't considered dirty. Pilot check. full suite 92/92.
* struct editor: loud save errors + guard unsaved editsblasty2026-07-10
| | | | | | | | | | | | | | | A struct with a field IDA's C parser rejects (notably '__unused', a predefined macro) failed to re-declare, but the failure was silent: the edit looked applied, then reloading showed the old definition (lost work). Now: - a failed declare shows a loud, styled 'save failed' status and, when it can, names the offending reserved field ('__unused') instead of IDA's empty 'Failed to parse'; the type is left unchanged and the editor keeps your text. - switching structs / starting new / closing with unsaved edits prompts to discard first (ConfirmScreen), so edits aren't silently reloaded away. Pilot: rejected save is loud/named/non-destructive; switching structs guards unsaved edits. full suite 91/91.
* clipboard: keyboard copy (OSC 52 + tmux) since mouse-select is capturedblasty2026-07-10
| | | | | | | | | | | | | The app captures the mouse (SGR tracking), so terminal/tmux drag-select can't grab text. Add explicit copy actions: - code views: 'y' copies the current line - struct editor: Ctrl+Y copies the selection, else the whole C definition App._copy() emits OSC 52 (Textual copy_to_clipboard) AND, inside tmux, pipes through 'tmux load-buffer -w -' — the combination is what actually reaches the system clipboard over tmux/ssh. Status shows the char count. full suite 87/87.
* struct editor: discoverable, confirmed delete (d/Del + ConfirmScreen)blasty2026-07-10
| | | | | | | | | | Delete was wired to the Del key but undiscoverable (modal has no footer) and unguarded. Add a 'd' alias (Del still works), and route delete through a small ConfirmScreen (Enter/y confirm, Esc/n cancel) since it's destructive. 'd' in the editor pane types normally; only the list triggers delete. Hint updated to 'd/Del delete'. Pilot: 'd' opens the confirm, Esc cancels (kept), Enter deletes. full suite 85/85.
* struct editor: C-style CRUD for local types (Ctrl+T)blasty2026-07-10
| | | | | | | | | | | | | | | | | New StructEditor overlay: left is the list of local structs/unions, right is an editable C definition. Enter loads a struct (reconstructed as C from its member layout via type_inspect), Ctrl+S declares it (create or update via declare_type), Ctrl+N starts a new one, Del deletes, Esc returns to the list then closes. Domain: Struct model + Program.list_structs / struct_source / declare_type / delete_type (anonymous $-types filtered). Delete needs a 'del_type' tool the ida-pro-mcp server doesn't currently expose; delete_type detects its absence and reports a clear message instead of failing. Create/read/update work fully. Pilot checks: open, list, view C, create, update-in-place, delete(-or-report), close. full suite 84/84.
* decompiler: fall back to disassembly when decompilation failsblasty2026-07-09
| | | | | | | | | | | Opening a function that Hex-Rays can't decompile (import/PLT stubs, mis-analyzed monsters) now shows the disassembly instead of a '/* decompilation failed */' panel. A separate _pref (the preferred view, set by Tab) is kept, so only the failing function falls back — the next decompilable function still opens as pseudocode. _apply_decomp ignores a stale result if you've navigated away. Pilot checks: failing func -> disasm (pref kept), next func -> pseudocode. full suite 77/77.
* default to the pseudocode view instead of disassemblyblasty2026-07-09
| | | | | | | | | | Opening a function now shows the decompiler by default (Tab still toggles to disassembly; the choice persists across opens). Startup focuses the decompiler view, and Ctrl+B / back focus the active view rather than hard-coding disasm. Tests: assert pseudocode-by-default on the first open, then normalize per-section (the disasm-scrolling/search/follow blocks switch to disassembly explicitly). full suite 75/75.
* names: command-palette fuzzy finder (Ctrl+N), overlay-firstblasty2026-07-09
| | | | | | | | | | | | | | Replace the docked names pane as the primary way to jump to symbols with a command-palette overlay: Ctrl+N opens SymbolPalette, type to fuzzy-find (scored subsequence match with matched-char highlighting), up/down to select, Enter to open, Esc to close. Mouse-clickable too. The docked FunctionsPanel now starts hidden and is still reachable/toggleable with Ctrl+B (classic table view, sort, filter, goto all unchanged); a code view takes focus on startup. Status hints 'Ctrl+N: find symbol'. Pilot checks for open/fuzzy/subsequence/select/close. Tests reveal the docked pane (Ctrl+B) for the existing table-driven checks. full suite 74/74.
* xrefs: pre-select the site the dialog was invoked fromblasty2026-07-09
| | | | | | | | | | | | | | Opening the xref dialog with 'x' now highlights the entry for the current site (the instruction/pseudocode line under the cursor), so a long xref list can be stepped through systematically without losing your place. Capture the cursor's ea span when 'x' is pressed (disasm: the instruction .. the next; decomp: the line's /*0xEA*/ anchor .. the next), and pre-select the xref whose frm matches exactly, else the one within that span. Threaded through _xrefs_{disasm,decomp} -> _xrefs_present -> XrefsScreen(preselect), which sets OptionList.highlighted (also scrolls it into view). Pilot check for the decomp path. full suite 68/68.
* xrefs: informative labels — function+offset, and section instead of '?'blasty2026-07-09
| | | | | | | | | | | | | | | The xref list showed just the containing function name, so multiple sites in the same function were indistinguishable ('sub_2300' repeated), and references not in any function collapsed to a bare '?'. - In-function sites now show fn_name+offset (e.g. main+0xb08), so each site is distinct and you can see where in the function it is. - Non-function sites (GOT/reloc data slots, loose thunks) show the section they live in (.got, .data.rel.ro, .text, LOAD, ...) instead of '?'. Add Program.sections()/section_of() over survey_binary's segment map (fetched once, cached lazily). Pilot checks for both. full suite 67/67.
* decompiler: xref jump lands the cursor on the reference token's columnblasty2026-07-09
| | | | | | | | | | | | | | | Previously an xref jump only positioned the pseudocode line; the cursor sat at column 0. Thread the referenced symbol's name (the token xrefs was invoked on, or the callee's name) from the xref dialog through _on_xref_chosen -> _do_navigate, and place the column at that token's whole-word start on the target line (_decomp_col_for, computed on the marker-stripped line so it matches the display). Works for both cross-function (via _apply_decomp cursor_x) and same-function (via DecompView.goto) jumps. Add a column assertion to the xref pilot test (asserts token-start when the token is on the line, line-start otherwise -- robust to a stale server Hex-Rays cache). full suite 65/65.
* decompiler: fix stale scroll frame on same-function jumpblasty2026-07-09
| | | | | | | | | | | | | | | DecompView.goto derived its scroll with _scroll_cursor_into_view() -> a plain scroll_to, which updates scroll_offset but doesn't repaint at the new offset (Textual only repaints on a rounded scroll change, and there's no layout pass since the pane isn't reloaded). So a same-function xref jump landed the cursor correctly but painted the old scroll until the next cursor move nudged a repaint. Route goto's scroll (derived or explicit) through _apply_scroll, which re-applies after the next refresh with layout=True -- the same pattern show() uses. The cross-function path already worked because reloading + clearing the loading cover forces a fresh paint. Document the gotcha in TEXTUAL_NOTES. full suite 64/64.
* decompiler: xref jump within the current function moves the cursorblasty2026-07-09
| | | | | | | | | | | | | | | The earlier xref->pseudocode-line fix only worked when the jump target was in a DIFFERENT function (which reloads the pane). When the target was inside the function already displayed, _show_active skipped the reload -> and never applied the new dec_cursor, so the cursor stayed put (the reported 'jumping to xref in decompile view doesn't work'; most xrefs land in the shown function). _open_entry now detects the already-loaded case and repositions the decompiler via a new DecompView.goto (moves cursor/scroll on the loaded text, no re-highlight). Toggle (Tab) still goes through _show_active untouched. Add a same-function xref-jump pilot check. full suite 64/64.
* decompiler: hide the per-line /*0xEA*/ address markers (clutter)blasty2026-07-09
| | | | | | | | | | | | | | | Hex-Rays appends a /*0xEA*/ VMA marker to every pseudocode line (we fetch with include_addresses). It's the only per-line address anchor the app has, so we can't turn it off server-side without breaking follow/comments/xref-jumps. Instead, strip it at display time: DecompView.show pulls each line's marker into a parallel _line_eas list and highlights the cleaned text. _line_ea now reads that list; _follow_decomp takes the line ea explicitly (was re-parsing the marker). Stripping only edits within lines, so indices still align with the domain's raw code (used by _decomp_line_for). Var-location comments (// [rsp+..]) are Hex-Rays output, not VMAs -> kept. full suite 63/63.
* comments: ';' sets a comment on the current line (disasm + pseudocode)blasty2026-07-09
| | | | | | | | | | | | | Bind ';' (NavMixin, so both code views) to a comment prompt for the line under the cursor. Resolve the line's address (_cursor_ea in disasm, the /*0xEA*/ marker in pseudocode), prefill any existing comment (parsed from the rendered '//' text), and apply via the set_comments tool; an empty value clears it. After setting, reuse the name-generation invalidation (bump_names) so Hex-Rays is force_recompiled lazily and the pseudocode/disasm refresh in place at the saved position -- the new '// comment' shows on the line. Marks the IDB dirty (Ctrl+S). Pilot checks for the pseudocode round-trip. full suite 63/63.