aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-07 15:21:10 +0200
committerblasty <blasty@local>2026-08-07 15:21:10 +0200
commitce9bcf91fc36a90ec952bd3f8787e5aadc8f1f7a (patch)
tree6db7bd8f2c2d6d8ccbd73a75b87bb7073e3330c7 /README.md
parentStop tracking the upstream port patch (diff)
downloadida-tui-ce9bcf91fc36a90ec952bd3f8787e5aadc8f1f7a.tar.gz
ida-tui-ce9bcf91fc36a90ec952bd3f8787e5aadc8f1f7a.tar.xz
ida-tui-ce9bcf91fc36a90ec952bd3f8787e5aadc8f1f7a.zip
README: rewrite, and fix the install it documented
The old one had drifted into a wall of prose that described the project to someone who already knew it. Rewritten around what a reader actually needs: what this is, what it needs, how to run it, the keys, and what is in it -- with the logo up top and the sharp edges at the bottom instead of a wall of disclaimers at the top. Corrected while checking every claim rather than trusting it: - pyproject depended on 'ida-codemode-mcp' from '../ida-codemode-mcp'. The package is 'ida-codemode' and the repo is '../ida-codemode', so 'uv sync' -- the documented install -- could not have worked for anyone. Fixed, and uv.lock regenerated (it now resolves, 39 packages). - test counts were 257/733, they are 302/788 - the keys table is generated from the real BINDINGS: c/d/a/p/u, o/O/B, s for split, ctrl+t for structs, " for strings, all verified against app.py - dropped the ida-pro-mcp/worker-era architecture section, which described files that no longer exist, and replaced it with the current layout - links docs/CODEMODE_UPSTREAM.md and .fastfeedback/SPEED.md, which did not exist when the README was last touched Every path and doc link in it is checked to exist.
Diffstat (limited to 'README.md')
-rw-r--r--README.md325
1 files changed, 139 insertions, 186 deletions
diff --git a/README.md b/README.md
index ffd15c6..405b65a 100644
--- a/README.md
+++ b/README.md
@@ -1,248 +1,201 @@
-# ida-tui
+<div align="center">
-A minimal, keyboard-first (mouse-capable) **TUI frontend for IDA Pro**, built with
-[Textual](https://textual.textualize.io/) and using
-[ida-codemode-mcp](../ida-codemode-mcp) as a Python library.
+<img src="logo-trans.png" alt="ida-tui" width="420">
-ida-tui attaches to databases through `ida_codemode.client.DatabaseHandle`. A
-matching database already open in the IDA GUI is reused; otherwise Code Mode
-reuses or starts a shared managed idalib worker. The TUI owns only a client lease,
-never the GUI or worker process.
+**IDA Pro in a terminal.** Listing, decompiler, graph — keyboard-first, mouse-capable.
-## ⚠️ Status: not ready for public consumption
+`disasm` · `pseudocode` · `cfg` · `hex` · `strings` · `structs` · `traces` · `rpc`
-This is a personal, actively-hacked-on project. It is **not** packaged, polished,
-or supported for general use. Expect sharp edges:
+</div>
-- Hardcoded paths and assumptions (e.g. a venv at `~/ida-venv`, a specific
- IDA/idalib layout).
-- No stable API, no versioning promises, no changelog — things move and break.
-- Requires a working IDA Pro + idalib install, which you must license and set up
- yourself.
-- Known open bugs (see the backlog below), including no handling of PLT/import
- stubs.
-- Basically undocumented beyond this file and `docs/`.
+---
-If you found this expecting a finished tool: it isn't one yet. Poke around, but
-don't file expectations. **Use at your own risk.**
-
-## What it does
-
-- A unified **IDA-style listing** (continuous disassembly interleaved with data /
- undefined heads) as the default code view; `F5`/`Tab` drops into the
- **decompiler (pseudocode)** for the function under the cursor. Both are
- line-virtualized and page lazily over the Code Mode database.
-- The startup splash draws the **real logo image** on terminals that speak the
- kitty graphics protocol (~10× the resolution of the block art), and falls back
- to `logo.ans` everywhere else. Support is detected by *asking the terminal*,
- not by sniffing `$TERM` — under a multiplexer that passes the protocol through,
- every environment variable you'd test is empty while the protocol works fine.
-- A **control-flow graph** (`space`, IDA's own key): the current function's basic
- blocks as boxes with routed, colour-coded edges (green taken / red fall-through
- / blue unconditional / purple loop), laid out with a proper layered
- (Sugiyama) algorithm. The boxes hold the *same listing rows* as the text view,
- so highlighting, renames, xrefs and comments all work inside them. `z` cycles
- three zoom levels, `m` toggles a minimap, `J`/`K` walk edges, and the mode is
- sticky — following a call lands in the callee's graph. Above 400 blocks it
- declines and says so, because nothing readable comes out at that size.
- Details in [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md).
-- A **Ghidra-style split view** (`s`): listing and pseudocode side by side, kept
- in cursor sync — the focused pane drives and the other highlights the linked
- region (every instruction a C line owns), following you across functions.
- `Tab` or a click switches which pane leads.
-- Keyboard navigation: follow (`enter`), xrefs (`x`, tagged call/read/write/
- offset), rename (`n`), retype (`y`), comment (`;`), incremental search (`/`
- `?`), history (`back`), hex view (`\`), and `home`/`end`/`shift+home` line
- motions.
-- **Literal formats** (`o`, IDA's own key): cycle how the number under the
- cursor is displayed — hex → decimal → binary → character → offset → IDA's
- own choice, `O` to go the other way. Only the stops that make sense for that
- value are visited (no `char` unless it prints as one, no `offset` unless the
- target is something you could name), so no press is a silent no-op. It works
- in the pseudocode too, on Hex-Rays' separate number formats. The opcode-bytes
- column, which used to own `o`, moved to `B`.
-
- A line usually holds more than one literal (`test byte ptr [rsi+rax*2+1], 20h`
- has two), so **the one the cursor is on is marked** — that mark is what `o`
- changes, and it keeps up as the text reflows (`0x30` ↔ `48` move everything
- after them). Land on something with no format of its own — a register — and it
- says so and names the operand that does, rather than quietly reformatting a
- different one.
-- A **functions panel** (fuzzy symbol palette on `Ctrl+N`), a **strings browser**
- (`"`, filterable, Enter jumps to the literal), **hex viewer**, **struct
- editor**, and inline **make code/data/function/string** edits.
-- A **command palette** (`Ctrl+P`) with the real ida-tui actions.
-- An optional unix-socket **RPC layer** to puppeteer the live TUI from another
- process (agent-driven RE / livestreaming). See `docs/RPC.md`.
-
-## Architecture (three layers, kept separate)
-
-- **`idatui/codemode_client.py`** — lifecycle and execution adapter. It leases a
- registered GUI/idalib instance with `DatabaseHandle`, waits for autoanalysis,
- normalizes errors, saves, and releases the lease. Address-centric operations
- are sent through Code Mode's `execute_python` surface and use its preloaded
- `ida-domain` `db` object.
-- **`idatui/domain.py`** — synchronous, thread-safe paging/caching
- (`FunctionIndex`, `DisasmModel`, `ListingModel`, decompile, xrefs, resolve).
- It has no process/database ownership logic.
-- **`idatui/app.py`** — the Textual app (virtualized `ScrollView`s, shared cursor/
- search/nav mixins, modals).
+```
+┌ ida-tui ─────────────────────────────────────────────────────────────────────┐
+│ .text:00002490 ; ---------- S U B R O U T I N E ---------- │
+│ .text:00002490 main proc near │
+│ .text:00002490 endbr64 │
+│ .text:00002494 push rbp ; ← cursor │
+│ .text:00002495 mov rbp, rsp │
+│ .text:00002498 sub rsp, 0B0h ; `o` → 176 │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
-`idatui/pool.py` retains LRU project leases. Releasing an entry never kills a GUI
-or another client's worker. See `docs/CODEMODE_PORT.md` for what maps to public
-ida-domain APIs and which remaining features require IDAPython inside the Code
-Mode execution sandbox.
+> **Status: personal project, actively hacked on.** No packaging, no versioning,
+> no support. It assumes a licensed IDA Pro and a venv at `~/ida-venv`. Things
+> move and break. Poke around; don't file expectations.
-## Requirements
+## Why
-- Python ≥ 3.11
-- IDA Pro 9.4+ with idalib configured
-- `ida-codemode-mcp` installed in the TUI environment (this checkout uses the
- editable sibling path `../ida-codemode-mcp`)
-- The ida-codemode IDA plugin installed so GUI databases register themselves
-- Textual ≥ 8 and Pygments ≥ 2 (`uv sync` installs both)
+IDA's own UI is excellent and it is a GUI. This is for the times you're in a
+terminal over SSH, in a tmux pane next to your notes, or driving RE from a
+script — and still want the listing, Hex-Rays, and a real control-flow graph.
-Code Mode's own worker launcher carries the correct Python environment; ida-tui
-no longer searches for a second Python or imports `ida_pro_mcp`.
+It is **not** a reimplementation of IDA. It's a frontend: IDA does the analysis,
+this draws it.
-## Running
+## Install
-Install the project and its TUI dependencies:
+Needs **Python ≥ 3.11**, **IDA Pro 9.4+ with idalib**, and
+[ida-codemode](https://github.com/HexRaysSA/ida-codemode) checked out next to
+this repo (`../ida-codemode`):
```sh
+git clone https://github.com/HexRaysSA/ida-codemode ../ida-codemode
uv sync
```
-Pass an executable/IDB path. If the plugin has registered a matching GUI session,
-ida-tui attaches to it; otherwise Code Mode opens a managed idalib database:
+Install the ida-codemode plugin in IDA too, if you want the TUI to attach to
+databases you already have open in the GUI.
-```sh
-./ida-tui /path/to/binary
-```
-
-With exactly one registered database, the path may be omitted:
+## Run
```sh
-./ida-tui
+./ida-tui /path/to/binary # attach to a GUI session, or open a managed database
+./ida-tui # attach, when exactly one database is registered
```
-When several databases are registered, the launcher lists their paths and asks
-for one explicitly. A newly managed single-binary database still needs a writable
-output location; projects stage binaries and IDBs in their sidecar directory.
+ida-tui never owns an IDA process. It takes a **lease**: a matching database open
+in the IDA GUI is reused, otherwise Code Mode starts or shares a managed idalib
+worker. Quitting drops the lease and leaves everyone else alone.
-Headerless blobs need to be told what they are — a raw firmware dump has no
-format to detect, and IDA falls back to x86 at address 0, which analyses to
-nothing:
+Headerless blobs have no format to detect — IDA falls back to x86 at address 0
+and analyses nothing, so say what it is:
```sh
./ida-tui fw.bin --processor arm --base 0x8000000
```
-ARM images that use Thumb need one more thing: press `t` on the listing to switch
-ARM/Thumb decoding at the cursor (it sets IDA's `T` register, and the segment to
-32-bit, since Thumb doesn't exist in AArch64).
+`--base` is a real address. These apply only when a database is being **created**;
+an existing IDB already records them. On ARM, `t` toggles ARM/Thumb decoding at
+the cursor and `T` scans a vector table for Thumb entry points.
-`--base` is a real address (Code Mode's typed loading address is also natural,
-so no paragraph conversion crosses the dependency boundary). In a project the
-options are recorded per binary. They apply only when Code Mode must create the
-first database; a registered or existing IDB already records them. Arbitrary
-`--ida-args` are rejected because `DatabaseHandle.open()` has no equivalent;
-processor, base, and loader/file type are the supported import surface.
+## Keys
-ida-tui never deletes unpacked IDA scratch files during discovery: those files
-may belong to a registered GUI or another Code Mode client. Registry locks and
-health probes are the ownership authority.
+| | |
+|---|---|
+| `enter` `escape` | follow / back |
+| `tab` `F5` | listing ↔ pseudocode |
+| `space` | control-flow graph (`z` zoom, `m` minimap, `J`/`K` walk edges) |
+| `s` | split view — listing and pseudocode, cursor-synced |
+| `g` `/` `?` | goto · search · search back |
+| `x` `n` `y` `;` | xrefs · rename · retype · comment |
+| `c` `d` `p` `u` `a` | make code · data · function · undefine · string |
+| `o` `O` `B` | cycle this literal's format · reverse · opcode bytes |
+| `\` `"` `ctrl+t` | hex · strings · structs |
+| `ctrl+n` `ctrl+p` | symbol palette · command palette |
+| `ctrl+s` `ctrl+l` `q` | save · reload as… · quit |
+| `F1` | all of them |
-## Execution traces
+## What's in it
-Load a [Tenet](https://github.com/gaasedelen/tenet) trace alongside the binary
-and explore it in time:
+**Listing** — one continuous IDA-style view: code, data and undefined runs
+together, with IDA's own colour tags and per-operand marks. Line-virtualized, so
+a 400 MB binary scrolls like a text file.
-```sh
-./ida-tui /path/to/binary --trace trace.0.log
-```
+**Decompiler** — Hex-Rays pseudocode with syntax highlighting, per-line address
+anchors, and rename/retype/comment that write back.
-A docked pane on the right shows the registers at the current timestamp (the
-ones the current instruction wrote are highlighted) and a timeline. `]` and `[`
-step one instruction forward and back; `}` and `{` step over a call by following
-the stack pointer. The code view follows.
+**Graph** (`space`) — the current function's basic blocks, laid out with a real
+layered (Sugiyama) algorithm and routed edges: green taken, red fall-through,
+blue unconditional, purple loop. The boxes hold the *same rows* as the listing,
+so highlighting, renames and xrefs work inside them. Above 400 blocks it declines
+and says so, because nothing readable comes out at that size.
+→ [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md)
-Both code views are painted with the execution trail: where you just came from,
-where you're about to go, and the instruction you're standing on. The pseudocode
-view is painted too — a trace records instructions, but `decomp_map` says which
-instructions each C line covers, so the same trail lands on the decompilation.
+**Split view** (`s`) — listing and pseudocode side by side. The focused pane
+drives; the other highlights every instruction the current C line owns.
-The dock also shows the **stack as of that instant**, read out of the trace.
-Bytes the trace never observed print as `??` rather than zeros — a trace knows
-what it saw and nothing else. The hex view (`\`) gets the same treatment: bytes
-the trace saw at this timestamp are shown in green over the file's own contents.
+**Literal formats** (`o`) — hex → decimal → binary → char → offset, IDA's own
+key. Only stops that change what you see are visited, so no press is a silent
+no-op. The literal under the cursor is *marked*, and the mark is what changes —
+it keeps up as the text reflows. Works on Hex-Rays' separate number formats too.
-Trace addresses are rebased onto the database automatically — a traced process
-is relocated, so nothing lines up until that's solved.
+**Execution traces** — load a [Tenet](https://github.com/gaasedelen/tenet) trace
+and move through time:
-Traces are recorded separately; see `~/dev/tenet/tenet-original/tracers/` for the
-QEMU tracer.
+```sh
+./ida-tui /path/to/binary --trace trace.0.log
+```
-## RPC / driving the TUI
+`]`/`[` step, `}`/`{` step over. Both code views are painted with the execution
+trail — including the pseudocode, since `decomp_map` knows which instructions
+each C line covers. The dock shows registers and the stack *as of that instant*;
+bytes the trace never saw print as `??`, not zeros. Trace addresses are rebased
+onto the database automatically.
-Give the TUI `--rpc <sock>` to expose a unix-socket control channel, then drive
-it from another pane:
+**RPC** — drive the live TUI from another process (agent-driven RE, livestreams):
```sh
./ida-tui /abs/path/bin --rpc /tmp/ida.sock
-python -m idatui.drive where # ergonomic terse-text helper
+python -m idatui.drive where # terse-text helper
python -m idatui.drive pc main # pseudocode of main
python -m idatui.drive rename sub_5BE0 foo # goto + rename
-python -m idatui.drive fmt dec # show this literal in decimal
```
-Or let `idatui.pane` spawn + manage TUI panes in tmux (see the idatui-rpc skill):
+→ [`docs/RPC.md`](docs/RPC.md)
-```sh
-python -m idatui.pane spawn --open /abs/path/bin # -> {sock, pane, ready}
-python -m idatui.pane list
-python -m idatui.pane stop --sock <sock>
+**Splash** — the logo renders as a real image on terminals that speak the kitty
+graphics protocol, `logo.ans` everywhere else. Support is detected by *asking the
+terminal*, not by sniffing `$TERM` (under a multiplexer, every variable you'd
+test is empty while the protocol works fine).
+
+## Layout
+
+```
+idatui/codemode_client.py lease a database, run operations through Code Mode
+idatui/remote_tools.py IDAPython that runs inside the database process
+idatui/domain.py paging + caching, synchronous and thread-safe
+idatui/app.py the Textual app
+idatui/graph.py CFG layout — pure, no IDA, no Textual
+idatui/rpc.py unix-socket control channel
```
-See `docs/RPC.md` for the full protocol.
+Layers stay separate: `domain.py` owns no processes, `graph.py` does no I/O
+(which is why its tests run offline in milliseconds), and `app.py` does no
+analysis.
## Tests
-`tests/run.py` is the front door — it runs every suite and prints one table.
-The live suites attach through Code Mode (a registered GUI database, or a managed
-idalib worker started on demand) for the given binary:
-
```sh
-python3 tests/run.py --fast # 257 checks, ~0.5s, any python3 — between edits
-python3 tests/run.py trace -x # only files matching "trace", stop at first failure
-python3 tests/run.py # all 733 checks, ~2m20s — before a commit
-python3 tests/run.py --list # what would run, and whether it needs IDA
+python3 tests/run.py --fast # 302 checks, <1s, any python3 — between edits
+python3 tests/run.py --list # what runs, and what needs IDA
+python3 tests/run.py # 788 checks, ~2m — before a commit
```
-It runs the suites **serially on purpose**: idalib contends hard enough that
-running them 4-up took the suite from 153s to 296s and got three of them killed
-mid-analysis. See the note in `tests/run.py`.
-
-Test files come in two kinds and **each one declares which** with a module-level
-`NEEDS_IDA` marker (`run.py` reads it without importing the file, and refuses to
-run if a file doesn't have one):
-
-- **pure** — stdlib only, no IDA, no worker, no binary. Seconds. This is what
- you run between edits.
-- **IDA** — spawns a real idalib worker on a real target and drives the headless
- Textual `Pilot` against it. Minutes, and needs a licensed IDA.
-
-The individual suites still run standalone, which is how you iterate on one:
+Every suite declares `NEEDS_IDA`; `--fast` runs only the pure ones (stdlib, no
+IDA, no database). The rest drive a headless Textual `Pilot` against a real
+database. Iterate on one with `--only`:
```sh
-~/ida-venv/bin/python tests/test_scenarios.py targets/echo # full UI suite
-~/ida-venv/bin/python tests/test_scenarios.py --only hex,rename
+~/ida-venv/bin/python tests/test_scenarios.py targets/echo --only hex,rename
```
+Before optimising or debugging a slow run, read
+[`.fastfeedback/SPEED.md`](.fastfeedback/SPEED.md) — per-suite timings, the known
+flake, and the four ways a test here wastes minutes.
+
## Docs
-- `docs/RPC.md` — the RPC protocol
-- `docs/CODEMODE_PORT.md` — port coverage, API gaps, and lifecycle semantics
-- `docs/PAGING_FINDINGS.md` — historical paging/scale findings
-- `docs/TEXTUAL_NOTES.md` — Textual pitfalls encountered
-- `docs/TUI_DRIVING_BLUEPRINT.md` — generalizing the driving layer
+| | |
+|---|---|
+| [`docs/RPC.md`](docs/RPC.md) | the RPC protocol |
+| [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md) | graph layout engine |
+| [`docs/CODEMODE_PORT.md`](docs/CODEMODE_PORT.md) | lifecycle, API gaps, what's still IDAPython |
+| [`docs/CODEMODE_UPSTREAM.md`](docs/CODEMODE_UPSTREAM.md) | findings sent to the Code Mode maintainers |
+| [`docs/PAGING_FINDINGS.md`](docs/PAGING_FINDINGS.md) | paging and scale findings |
+| [`docs/TEXTUAL_NOTES.md`](docs/TEXTUAL_NOTES.md) | Textual pitfalls, the hard way |
+
+## Known sharp edges
+
+- PLT/import stubs: following a libc symbol lands on `extrn X:near`, which
+ Hex-Rays can't decompile. No pretty-printing yet.
+- Xref-select can land on the wrong line in the `x` popup.
+- No "discard changes and quit": a managed worker saves when its last lease
+ closes, so the TUI can't promise your edits weren't persisted.
+- A GUI user editing the same database won't invalidate our caches — Code Mode
+ has no change feed yet (see `docs/CODEMODE_UPSTREAM.md`).
+
+<div align="center">
+<sub><a href="https://sl0p.foo">sl0p.foo</a></sub>
+</div>