aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py31
1 files changed, 29 insertions, 2 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 8baff74..76aff62 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -2356,6 +2356,7 @@ class GraphView(NavMixin, ScrollView, can_focus=True):
Binding("0", "goto_entry", "Entry", show=False),
Binding("z", "zoom", "Zoom"),
Binding("m", "minimap", "Minimap", show=False),
+ Binding("e", "engine", "Engine", show=False),
Binding("f", "center", "Centre", show=False),
Binding("ctrl+d", "pan(12)", "½↓", show=False),
Binding("ctrl+u", "pan(-12)", "½↑", show=False),
@@ -2388,6 +2389,9 @@ class GraphView(NavMixin, ScrollView, can_focus=True):
self._blocks: dict[int, object] = {}
self._zoom = 0
self._show_minimap = True
+ #: layout backend; "auto" prefers triskel where it is installed and the
+ #: function is small enough for it. Cycled with `e`.
+ self._engine = "auto"
self._mini_cache: tuple | None = None
self._drag: tuple[int, int, float, float] | None = None
self._drag_map = False # the drag started on the minimap
@@ -2423,7 +2427,8 @@ class GraphView(NavMixin, ScrollView, can_focus=True):
return
blocks = [graph.Block(id=b.id, start=b.start, end=b.end,
succs=list(b.succs)) for b in self.fc.blocks]
- self.lay = graph.layout(blocks, self._sizer, entry=self.fc.entry)
+ self.lay = graph.layout(blocks, self._sizer, entry=self.fc.entry,
+ engine=self._engine)
self.virtual_size = Size(self.lay.width + 2, self.lay.height + 1)
def _rows(self, nid: int):
@@ -2657,6 +2662,26 @@ class GraphView(NavMixin, ScrollView, can_focus=True):
self.refresh()
self.app._status(f"graph: minimap {'on' if self._show_minimap else 'off'}")
+ def action_engine(self) -> None:
+ """Cycle the layout engine and redraw the same function with it.
+
+ The two engines disagree about shape more than about correctness --
+ native draws wide and short, triskel narrow and tall with far fewer
+ crossings -- and which one reads better genuinely depends on the
+ function. Cheaper to look than to argue.
+ """
+ from . import graph_triskel
+ choices = ["auto", "native"] + (["triskel"] if graph_triskel.available()
+ else [])
+ self._engine = choices[(choices.index(self._engine) + 1) % len(choices)]
+ self._relayout()
+ self._clamp_cursor()
+ self._center_cursor()
+ self.refresh(layout=True)
+ got = self.lay.stats["engine"] if self.lay else "?"
+ note = "" if graph_triskel.available() else " (pytriskel not installed)"
+ self.app._status(f"graph: engine {self._engine} \u2192 {got}{note}")
+
def action_center(self) -> None:
self._center_cursor()
self.refresh()
@@ -3664,6 +3689,7 @@ _HELP = (
("0", "jump to the entry block"),
("z", "zoom: full \u2192 compact \u2192 collapsed"),
("m", "show/hide the minimap"),
+ ("e", "layout engine: auto \u2192 native \u2192 triskel"),
("f", "centre on the current block"),
("Enter", "follow (stays in the graph if it lands here)"),
("drag / click", "pan / put the cursor in a block"),
@@ -6502,9 +6528,10 @@ class IdaTui(App):
return
s = gv.lay.stats
loops = f", {s['back']} loop{'s' if s['back'] != 1 else ''}" if s["back"] else ""
+ eng = "" if s.get("engine") == "native" else f", {s.get('engine')}"
self._status(
f"{gv.fc.name} @ {gv.fc.func_ea:#x} [graph: {s['blocks']} blocks, "
- f"{s['edges']} edges{loops}] "
+ f"{s['edges']} edges{loops}{eng}] "
f"z=zoom({gv.ZOOMS[gv._zoom]}) m=map J/K=edge space=text")
def on_graph_view_cursor_moved(self, msg: "GraphView.CursorMoved") -> None: