aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-10 13:02:05 +0200
committerblasty <blasty@local>2026-07-10 13:02:05 +0200
commitae3399da08a54e3ecdc8b4c3e622efbf54b55dae (patch)
treebbe05236c8e077fe39dd276e012839c9048bccfc
parentstruct editor: loud save errors + guard unsaved edits (diff)
downloadida-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.py21
-rw-r--r--tests/test_tui.py5
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})())