diff options
| author | blasty <blasty@local> | 2026-07-10 13:02:05 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-10 13:02:05 +0200 |
| commit | ae3399da08a54e3ecdc8b4c3e622efbf54b55dae (patch) | |
| tree | bbe05236c8e077fe39dd276e012839c9048bccfc | |
| parent | struct editor: loud save errors + guard unsaved edits (diff) | |
| download | ida-tui-ae3399da08a54e3ecdc8b4c3e622efbf54b55dae.tar.gz ida-tui-ae3399da08a54e3ecdc8b4c3e622efbf54b55dae.tar.xz ida-tui-ae3399da08a54e3ecdc8b4c3e622efbf54b55dae.zip | |
struct editor: auto-format the definition on save
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.
| -rw-r--r-- | idatui/app.py | 21 | ||||
| -rw-r--r-- | tests/test_tui.py | 5 |
2 files changed, 23 insertions, 3 deletions
diff --git a/idatui/app.py b/idatui/app.py index 271d8f7..32a0698 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1334,9 +1334,18 @@ class StructEditor(ModalScreen): return m = re.search(r"\b(?:struct|union)\s+([A-Za-z_]\w*)", text) name = m.group(1) if m else None - self.app.call_from_thread(self._after_save, name, err, text) + # On success, pull the canonical source back so the editor shows the + # normalized layout/types (auto-format on save). + formatted = None + if not err and name: + try: + formatted = self._program.struct_source(name) + except Exception: # noqa: BLE001 + formatted = None + self.app.call_from_thread(self._after_save, name, err, text, formatted) - def _after_save(self, name: str | None, err: str | None, text: str) -> None: + def _after_save(self, name: str | None, err: str | None, text: str, + formatted: str | None) -> None: if err: # IDA's parse error is usually empty/cryptic; name the likely cause. msg = err.strip() @@ -1348,7 +1357,13 @@ class StructEditor(ModalScreen): self._set_status(f"save failed — {msg}", error=True) return self._loaded = name - self._loaded_src = text.strip() # editor now matches the saved type + if formatted: + # Reformat the editor to IDA's canonical layout (keeps cursor at top). + ta = self.query_one("#se-edit", TextArea) + ta.text = formatted + self._loaded_src = formatted + else: + self._loaded_src = text.strip() # editor already matches the saved type if getattr(self.app, "_dirty", None) is not None: self.app._dirty = True # unsaved-to-disk until Ctrl+S in the app self._refresh(select=name) diff --git a/tests/test_tui.py b/tests/test_tui.py index e990bef..6a8f898 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -180,6 +180,11 @@ async def run(db): await wait_until(pilot, lambda: any(s.name == sname for s in se._structs), 15) check("Ctrl+S declares a new struct", any(s.name == sname for s in se._structs), "not created") + # save auto-formats the (one-line) definition to canonical layout + await wait_until(pilot, lambda: "\n" in ta.text, 10) + check("save auto-formats the definition in the editor", + ta.text.count("\n") >= 3 and f"struct {sname}" in ta.text + and not se._is_dirty(), f"text={ta.text[:50]!r}") # update it (add a field -> member count grows) idx = next(i for i, s in enumerate(se._structs) if s.name == sname) se.on_option_list_option_selected(type("E", (), {"option_index": idx})()) |
