aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/pane.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 23:03:10 +0200
committerblasty <blasty@local>2026-07-25 23:03:10 +0200
commitb8d9aa4fce78f4523cffaba8b46095d2dfb66310 (patch)
tree97e73ec6b027a19cbeafe026aaf8b90bad0773d1 /idatui/pane.py
parentprojects phase 3: follow an import into the binary that implements it (diff)
downloadida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.gz
ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.tar.xz
ida-tui-b8d9aa4fce78f4523cffaba8b46095d2dfb66310.zip
projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs
Three things, all following from phase 3 making cross-binary jumps ordinary. **A cross-binary jump was a one-way door.** Nav history is per-binary, so arriving in another binary — a project search hit, or now following an import into the library that implements it — landed you in an empty history with nothing to take you back. _switch_then_goto records the binary it came FROM, and action_back falls through to that hop once local history is spent: Esc walks back through the function you were in, then the binary you were in. Manual Ctrl+O switching records nothing, because that isn't navigation. **Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms the binary providing the most of this one's imports — where a follow is most likely to go, so its startup is paid before you ask for it. "Next in the list" would have been arbitrary; phase 3 gave us something better to ask. pool.prewarm() refuses rather than making room. Evicting a binary the user visited to speculatively load one they haven't is a straight downgrade, and it throws away that binary's caches as well; at a tight budget pre-warm just does nothing. The cost of a worker that doesn't exist yet can only be estimated, so it uses the largest resident one (same program, different database) — and if that estimate proves wrong, the speculative worker is the one evicted, never a chosen one. **Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists the inventory (active / resident / indexed / where Esc returns to) and `switch {binary,addr?}` makes another active — with an address it takes the search-hit path, so it records a hop. state gains `binary` and `hops`, which it should have had the moment project mode existed. Verified on real sessions: drive binaries/switch against an echo+cat project pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc (provider of echo's imports) and warming it after an evict. tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts nothing when refusing, ignores unknown labels) and +4 project UI (jump records the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool, 33/0 project. Left open: project-level persistence across sessions.
Diffstat (limited to 'idatui/pane.py')
-rw-r--r--idatui/pane.py32
1 files changed, 24 insertions, 8 deletions
diff --git a/idatui/pane.py b/idatui/pane.py
index e34b822..53afef3 100644
--- a/idatui/pane.py
+++ b/idatui/pane.py
@@ -131,15 +131,20 @@ def spawn(args) -> int:
if not os.environ.get("TMUX"):
print("error: not inside tmux (spawn creates a tmux pane)", file=sys.stderr)
return 2
- if not args.open:
- print("error: pass --open <binary>", file=sys.stderr)
+ if not args.open and not getattr(args, "project", None):
+ print("error: pass --open <binary> or --project <file>", file=sys.stderr)
return 2
sock = args.sock or os.path.join(_sockdir(), f"idatui-{secrets.token_hex(3)}.sock")
- target = os.path.abspath(os.path.expanduser(args.open))
- if not os.path.exists(target):
+ project = (os.path.abspath(os.path.expanduser(args.project))
+ if getattr(args, "project", None) else None)
+ target = os.path.abspath(os.path.expanduser(args.open)) if args.open else None
+ if target is not None and not os.path.exists(target):
print(f"error: no such binary: {target}", file=sys.stderr)
return 2
+ if project is not None and target is None and not os.path.exists(project):
+ print(f"error: no such project: {project}", file=sys.stderr)
+ return 2
# Reap workers leaked by previously-stopped/crashed panes so we don't spawn
# into a full IDA_MCP_MAX_WORKERS (which makes the new TUI hang forever,
@@ -151,7 +156,15 @@ def spawn(args) -> int:
# the command the pane runs: the launcher spawns a private idalib worker for
# this binary and becomes the TUI, so kill-pane tears the whole thing down.
- inner = [args.python, "-m", "idatui.launch", target, "--rpc", sock]
+ if project is not None:
+ # launch takes: --project FILE [binaries...]; extra binaries are added to
+ # the project (and a missing project file is created from them).
+ inner = [args.python, "-m", "idatui.launch", "--project", project]
+ if target is not None:
+ inner.append(target)
+ inner += ["--rpc", sock]
+ else:
+ inner = [args.python, "-m", "idatui.launch", target, "--rpc", sock]
cmd = f"cd {REPO!r} && exec " + " ".join(_q(a) for a in inner)
split = ["split-window", "-v" if args.vertical else "-h",
@@ -166,8 +179,8 @@ def spawn(args) -> int:
split.append(cmd)
pane = _tmux(*split)
- row = {"sock": sock, "pane": pane, "target": target,
- "kind": "open", "started": time.time()}
+ row = {"sock": sock, "pane": pane, "target": project or target,
+ "kind": "project" if project else "open", "started": time.time()}
reg = [r for r in _load_registry() if r.get("sock") != sock]
reg.append(row)
_save_registry(reg)
@@ -301,8 +314,11 @@ def main(argv: list[str]) -> int:
sub = p.add_subparsers(dest="cmd", required=True)
sp = sub.add_parser("spawn", help="open a TUI pane and wait until ready")
- sp.add_argument("--open", metavar="PATH", required=True,
+ sp.add_argument("--open", metavar="PATH",
help="binary to open (its dir must be writable)")
+ sp.add_argument("--project", metavar="FILE",
+ help="project file to open instead of a single binary; "
+ "any --open paths are added to it (created if absent)")
sp.add_argument("--sock", help="RPC socket path (default: auto in $XDG_RUNTIME_DIR)")
sp.add_argument("--python", default=DEFAULT_PY, help=f"python for the TUI ({DEFAULT_PY})")
sp.add_argument("--vertical", action="store_true", help="split vertically (stacked)")