aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* listing: F5/Tab decompiles the function under the cursor (IDA-style) — step 2blasty2026-07-232-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 1blasty2026-07-233-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)blasty2026-07-232-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)blasty2026-07-232-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)blasty2026-07-233-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; ↵blasty2026-07-231-3/+11
| | | | struct-expand/unify deferred
* perf: stream the listing incrementally instead of load_all on open (M4)blasty2026-07-232-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)blasty2026-07-234-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)blasty2026-07-232-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)blasty2026-07-232-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)blasty2026-07-232-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 paletteblasty2026-07-232-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)blasty2026-07-232-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)blasty2026-07-235-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)blasty2026-07-224-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 walkerblasty2026-07-221-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)blasty2026-07-225-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)blasty2026-07-223-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)blasty2026-07-224-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.
* drive pc: render pseudocode in the TUI, not just the driveruser2026-07-111-3/+32
| | | | | | | | The pc command called only the read-only pseudocode RPC (runs off the UI loop, never touches the screen), so LLM-driven sessions showed nothing on the live pane. Compose goto + toggle_view + search so the real TUI navigates to the function, makes the decomp pane the visibly-active view, and jumps the cursor to the needle — then return the same text as before.
* docs: blueprint for generalizing the TUI-driving layer (e.g. gdb)blasty2026-07-101-0/+355
| | | | | | | | | | A design reference for bringing the idatui spawn/drive/introspect experience to other terminal apps. Captures the invariants worth keeping, the reusable core vs per-target Adapter split, a target taxonomy (embedded / control- channel / black-box PTY), the three hard problems (injection, settle, introspection) with technique gradients, tmux as the universal substrate, and a concrete gdb worked example (TUI pane + gdb-Python plugin as the adapter). No code.
* app: multi-line comments via literal \\n (long notes were clipping)blasty2026-07-103-5/+17
| | | | | | | | | 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.
* drive: add save + retype; point docs/skills at the ergonomic frontendblasty2026-07-102-4/+24
| | | | | | | | Round out idatui.drive with 'save' and 'retype <fn> <proto>' so the whole common RE loop (orient/understand/act/persist) has a terse command. Update docs/RPC.md and both skills (idatui, idatui-rpc) to recommend idatui.drive as the day-to-day driving surface, with rpcclient/raw as the fallback for unwrapped verbs.
* drive: ergonomic RE helper over the RPC socket (terse text, auto socket)blasty2026-07-102-0/+228
| | | | | | | | | | | | | | | | Driving via rpcclient meant re-exporting the socket every fresh shell, long invocations, and piping ~every call through python -c to pull one field or grep pseudocode. idatui.drive fixes that: it auto-resolves the socket (the single live pane from the registry), prints compact text instead of JSON, and bundles the common gestures: where | go | pc <fn> [substr] | dis <fn> [n] | callees/callers <fn> | names <substr> | rename <old> <new> | mv old=new... | note <fn> <text> | screen | raw <method> k=v So 'goto+rename+parse' becomes 'drive rename old new', and 'pseudocode | python -c grep' becomes 'drive pc fn needle'. rpcclient stays the raw transport. rpc_smoke drives it end-to-end (29 green).
* app: comment on a no-address line -> function comment (was refused)blasty2026-07-102-2/+42
| | | | | | | | | | 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-104-5/+35
| | | | | | | | | | | | | | 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.
* pane: auto-start the ida-pro-mcp supervisor if it's downblasty2026-07-102-1/+164
| | | | | | | | | | | | Before creating the TUI pane, spawn probes 127.0.0.1:8745 and, if nothing is listening, launches ./spawn.sh in its own detached tmux pane and waits for the port (server_started/server_pane are reported in the JSON). Only for a local server, never a duplicate (spawn.sh binds the port), and it's not killed on 'stop' (shared across TUIs). --no-ensure-server opts out; IDATUI_SERVER_CMD overrides the launch command (used by the test). tests/pane_smoke.py exercises the machinery with a dummy port-binder in a pane (start / detect / idempotent / remote-guard), 5 green.
* rpc: cursor_on + word= edits, single-driver gate, colored screenblasty2026-07-103-17/+118
| | | | | | | | | | | | | | cursor_on {word,line?,occurrence?} places the cursor on a token, verified with the app's own tokenizer (so 'main' won't match inside 'domain'); rename/retype/follow gain an optional word= that runs it first — the ergonomic way to edit a named symbol without a manual view+cursor dance. Single-driver gate: the socket now serves one client at a time; a second concurrent connection is refused with 'busy' (no multi-driver support yet, by request). Sequential connections are unaffected. screen gains format=text|html|svg (html/svg are colored — for an out-of-band web viewer). rpc_smoke: 24 green.
* pane: spawn/stop/list idatui TUI panes in tmux for agent-driven REblasty2026-07-101-0/+243
| | | | | | | | | | | | python -m idatui.pane spawn --open <bin> (or --db <session>) splits a tmux pane running the TUI on a fresh RPC socket, blocks until it reports ready (ping.ready; --open runs full auto-analysis so the default wait is generous), and prints {sock,pane,ready,functions,module} as JSON. The agent then drives it via idatui.rpcclient and tears it down with 'stop --sock <s>' (graceful quit -> kill-pane -> unlink). A small registry in $XDG_RUNTIME_DIR tracks panes for 'list'/'stop'. Verified end-to-end: spawn -> goto/pseudocode -> list -> stop.
* rpc: readiness, quit, methods discovery, not-ready guardblasty2026-07-102-4/+94
| | | | | | | | | | | Spawn-and-drive needs to know when the freshly-launched TUI is usable: ping/state now carry {ready,functions,complete} (cheap; no health call) and ping adds the module name. Add a graceful 'quit' verb (acks, then exits on a short delay so the reply still flushes) and a self-documenting 'methods' table. Program-dependent verbs are refused with a clear 'not ready' instead of exploding in a worker while still loading. rpc_smoke covers readiness, methods and quit (21 green).
* rpc: more verbs — structured reads, modal select, in-view search, saveblasty2026-07-103-0/+174
| | | | | | | | | | | | Introspection (offloaded to a thread so a big fetch can't freeze the render): pseudocode (full body), disassembly (bounded), xrefs_to/from (structured), resolve (name->ea). These let an agent reason about a function and the call graph without scraping screen(). Semantic: select (choose the highlighted/nth item in the open xrefs or symbol-palette list and activate it — the natural follow-up to xrefs/ symbols), search (/ or ? incremental in the active code view), save (Ctrl+S). rpc_smoke exercises all of them end-to-end (18 green).
* docs: RPC protocol reference (docs/RPC.md)blasty2026-07-101-0/+98
|
* rpc: stdlib client + CLI (idatui.rpcclient)blasty2026-07-101-0/+133
| | | | | | | | | | Drive the TUI from another tmux pane with no deps: python -m idatui.rpcclient --sock <path> goto target=main python -m idatui.rpcclient keys g m a i n enter python -m idatui.rpcclient screen # dumps the raw screen text Also importable as RpcClient for the agent. Socket via --sock or IDATUI_RPC_SOCK. Booleans are coerced; numerics/addresses stay strings (the server coerces numeric params).
* rpc: semantic verbs (typed-with-delay ops + fast movement)blasty2026-07-102-3/+165
| | | | | | | | | | | | | | Layer high-level verbs over the raw injection: goto/open, rename, comment, retype, follow, back, toggle_view, hex, xrefs, symbols, structs, close, plus fast move/cursor. Editing/goto verbs type through the real prompts with a per-char delay (default 35ms) so viewers see it typed; movement uses bare keypresses with a pump-only settle to stay snappy. Each verb settles on an op-specific predicate where one exists (goto lands on the target, follow grows the nav stack, toggle flips _active, modals open), so the returned state is never stale. rpc_smoke now drives the semantic path end-to-end (rename round-trip via the prompt-fill seam), 11 green.
* rpc: unix-socket puppeteering server (raw keys + introspection + screen)blasty2026-07-104-2/+418
| | | | | | | | | | | | | | | 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).
* sync: extract shared settle/wait helpers (idatui/_sync.py)blasty2026-07-102-7/+93
| | | | | | | | | | | Factor the pilot suite's ad-hoc 'poll until the UI reacted' loop into one reusable module so the upcoming RPC driver and the tests share a single source of truth for quiescence. wait_for() takes the yield strategy (pilot.pause vs asyncio.sleep); drain()/workers_idle()/settle() give the live driver a 'block until quiescent, then until pred holds' primitive built on Textual's own _wait_for_screen + the worker manager. Ctx.wait now delegates to wait_for; suite unchanged (101 green).
* tests: retire the monolithic test_tui.py (superseded by test_scenarios)blasty2026-07-101-1236/+0
| | | | | | The scenario suite now covers every behavior the monolith did (all check names accounted for, modulo renames), so drop the old single-session runner.
* tests: isolated scenario suite (parity with the monolith, fast --only)blasty2026-07-101-0/+1272
| | | | | | | | | | | | | | | | | | | | Replace the 1200-line monolithic run() — one giant pilot session whose 100 checks shared mutable app state, so a single break cascaded and every edit meant re-running the whole ~49s suite — with independent @scenario functions over one shared boot. Each scenario re-establishes its own function context and is wrapped so a crash (or a check failure) fails that scenario alone instead of aborting the rest. A reset() baselines between scenarios: dismiss modals, hide the names pane and input widgets, clear the filter, default _pref=decomp. A Ctx helper wraps app+pilot (check/wait/press/open/goto_ui, biggest(), pick_decomp_ref(), ...). CLI: --only <substr,...> runs a subset in a few seconds (structs in ~1.5s vs 49s), --stop-after, --list. Per-scenario timing is printed, which is what surfaced a decomp-default view-flip that had been passing a jump+back check trivially. Full run: 101 checks green in ~41s.
* hex view: file offsets + 'g' goto (dedicated input)blasty2026-07-103-14/+116
| | | | | | | | | | | | | 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-103-5/+421
| | | | | | | | | | | | | | | | 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-104-23/+348
| | | | | | | | | | | | | | | | 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-102-3/+23
| | | | | | | | 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-102-8/+66
| | | | | | | | | | | | | | | 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.