<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/experiments, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/experiments?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/experiments?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-08-21T10:15:15Z</updated>
<entry>
<title>reformat: ruff format + import sort, mechanically (see ruff.toml)</title>
<updated>2026-08-21T10:15:15Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:14:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=02d02417800184fb76cd0245cdaa94c437aa4081'/>
<id>urn:sha1:02d02417800184fb76cd0245cdaa94c437aa4081</id>
<content type='text'>
No behavior. Listed in .git-blame-ignore-revs (next commit).
</content>
</entry>
<entry>
<title>Merge PR #1 from mrexodia: Windows support and auto-refresh on events</title>
<updated>2026-08-21T10:09:21Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:09:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=9541043c9dee7973c140159b3af561c6d0405e9e'/>
<id>urn:sha1:9541043c9dee7973c140159b3af561c6d0405e9e</id>
<content type='text'>
https://github.com/blasty/ida-tui/pull/1 — from the ida-nexus (né
ida-codemode) maintainer: kitty-graphics fallback for platforms without
termios, Ctrl+R view refresh, typed remote operations through ida_nexus
RemoteModule, live auto-refresh from other clients' IDB events (upstream
item 7), discard-without-save (item 6), and the ida-nexus 0.7.0 rename.

tests/test_kittygfx.py is the union of both sides' files: our escape-
construction checks plus the PR's cross-platform fallback checks.
</content>
</entry>
<entry>
<title>splash: give the logo a placement id so re-anchoring replaces, not stacks</title>
<updated>2026-08-21T10:08:42Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-21T10:08:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=7a3b198a904da186cc2860df059b063db24282ea'/>
<id>urn:sha1:7a3b198a904da186cc2860df059b063db24282ea</id>
<content type='text'>
A kitty placement is identified by (image id, placement id); an a=p with no
p= key is anonymous and every one stacks another copy. The splash re-anchors
on every progress note (~5/s), so a 60s load ended with ~300 placements of an
RGBA logo alpha-compositing over each other -- soft edges creeping to solid,
and the terminal re-rendering all of them per frame.

Same pair every time (LOGO_PLACEMENT) = the terminal REPLACES the placement,
so re-anchoring is free and atomic. That is also why on_resize no longer
clear()s first (that showed a hole for a frame), and why a shrunk-to-nothing
region now drops the placement instead of leaving a stale one anchored.

tests/test_kittygfx.py pins the escapes (pure, stdlib);
experiments/splash_place_count.py counts what a real splash sends.
</content>
</entry>
<entry>
<title>Switch to ida-nexus</title>
<updated>2026-08-20T21:55:52Z</updated>
<author>
<name>Duncan Ogilvie</name>
<email>mr.exodia.tpodt@gmail.com</email>
</author>
<published>2026-08-20T21:55:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f4c1d9b5497fd0d38137b6b345e8171b307c1117'/>
<id>urn:sha1:f4c1d9b5497fd0d38137b6b345e8171b307c1117</id>
<content type='text'>
</content>
</entry>
<entry>
<title>Adopt idb_events and remote module features from ida-codemode</title>
<updated>2026-08-20T21:42:42Z</updated>
<author>
<name>Duncan Ogilvie</name>
<email>mr.exodia.tpodt@gmail.com</email>
</author>
<published>2026-08-20T21:42:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=f3715d8de0d255c8b14710acfa120ccb9ea953fd'/>
<id>urn:sha1:f3715d8de0d255c8b14710acfa120ccb9ea953fd</id>
<content type='text'>
</content>
</entry>
<entry>
<title>docs: be honest that the triskel fork is unpublished</title>
<updated>2026-08-15T09:16:52Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-15T09:16:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=96ce4ccb2433600e710929a48b55238721ecb061'/>
<id>urn:sha1:96ce4ccb2433600e710929a48b55238721ecb061</id>
<content type='text'>
The install instructions pointed at ~/dev/triskel, a path only I have, so a
reader could not act on them. Say plainly that the patched pytriskel is not
released yet and that native is the supported default.

Also drops the .fastfeedback/SPEED.md references and refreshes a stale
branch name and commit hash in bench_ops.py.
</content>
</entry>
<entry>
<title>docs: drop the triskel evaluation artifacts</title>
<updated>2026-08-15T09:12:11Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-15T09:11:36Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=46e136680d4f2e80c4caf5d83641be786b7c47cb'/>
<id>urn:sha1:46e136680d4f2e80c4caf5d83641be786b7c47cb</id>
<content type='text'>
TRISKEL_EVAL.md and graph_compare.py were intermediary working docs from
choosing the SESE engine. Triskel now ships alongside the native layout
engine, so the evaluation no longer needs to be in the tree; GRAPH_VIEW.md
keeps the part that describes shipping behaviour.

Also drops the local-only ~/dev/triskel/PATCHES.md pointer from pyproject.
</content>
</entry>
<entry>
<title>domain: build Head positionally, and drop a dead test from _as_int</title>
<updated>2026-08-09T22:40:28Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-09T22:40:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=fccf1c3d6f519636b0b11c09cd0eea138b68fb98'/>
<id>urn:sha1:fccf1c3d6f519636b0b11c09cd0eea138b68fb98</id>
<content type='text'>
Head is the most-constructed object in the codebase -- 227k of them to stream
one bash -- and from_raw was paying for two things it did not need.

_as_int ended in a ternary whose arms were BOTH int(v, 16), so the
isinstance+startswith in front of them decided nothing and ran on every
address the client parses (55k times per 60 pages). Removed; the function is
now the isinstance it always was.

Head(...) was built by keyword, which makes the tuple match names against
fields; positional is the same object with none of that. Address conversion
is inlined for the same reason from_raw exists at all.

Together 30% off Head construction (2.27 -&gt; 1.59ms per 2003 rows, 0.34us a
row), with output verified identical against the previous implementation over
both skeleton and full rows. isinstance was kept over the 5%-faster
"type(v) is int" because the two differ for bool and int subclasses, and that
is not a trade worth making for 5% of 12% of a page.

End to end this is ~2.5% of bash's boot (3117 -&gt; 3065ms): Head construction
was ~12% of the client half, and the client half is ~54% of what is left.

Adds experiments/profile_client.py -- profile_remote.py's counterpart for the
half of a page load that happens outside the database process, which is where
the remaining time now is.

Full gate: 1050 passed.
</content>
</entry>
<entry>
<title>Skeleton pages: stop rendering 227k rows to count them (3x boot)</title>
<updated>2026-08-09T22:14:51Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-09T22:14:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b74179c2c8038f9fe55c880bf76fea89b903d3ef'/>
<id>urn:sha1:b74179c2c8038f9fe55c880bf76fea89b903d3ef</id>
<content type='text'>
ListingView._grow streams the entire segment in the background for one
reason: to learn how many rows it has, so the scrollbar and paging are
right. It did that by rendering every row in full -- 227,500 rows of a
1.2MB bash, 911 backend calls, 9.3 seconds -- essentially none of which is
ever looked at.

generate_disasm_line is 22x the cost of the walk around it, so heads() gains
text=False: a SKELETON page with the same rows at the same addresses with the
same kinds and sizes, and no rendered text. Measured identical structurally
(rows, addresses, kinds, sizes and cursor all match a real page) which is
what makes one swappable for the other later. It also skips the digest
(nothing to go stale) and lets the client skip the bulk opcode read, so a
page costs ONE round trip instead of two.

Client side is deliberately tiny, because the machinery already existed: a
skeleton page is just a page whose text is stale. It is marked with a
sentinel generation no _text_gen can equal, and the FIRST read of it goes
through the same _ensure_text/_ensure_page path a rename uses -- which
already refetches a page by address, verifies the structure still lines up
and splices it in. Two staleness gates learn to fire for _skeleton as well
as _renamed; that is the whole integration.

bash boot: 911 calls / 9.26s -&gt; 456 calls / 3.12s, 3.0x. The trade is that a
page you actually display is fetched twice (3.3ms + 9.4ms vs 9.4ms), paid
only for what is shown. _prime still loads real pages, so the viewport you
land on is never a skeleton.

The failure mode is BLANK ROWS, not an exception, and nothing in the suite
scrolled far enough to see one: _prime renders the first ~1000 rows for real,
so a test that pages down a few screens passes against a completely broken
implementation. The new scenario reads deep rows through both the model and
the render path, and asserts materialising changes neither the row count nor
the walk. Verified by reverting the two gates: it fails with text=''.

Full gate: 1050 passed.
</content>
</entry>
<entry>
<title>remote_tools: hoist the IDAPython imports to module scope (~3%)</title>
<updated>2026-08-09T21:35:32Z</updated>
<author>
<name>blasty</name>
<email>peter@haxx.in</email>
</author>
<published>2026-08-09T21:35:32Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=0f60e84f9d753ba0a6aa33076ebeb7fd4068238e'/>
<id>urn:sha1:0f60e84f9d753ba0a6aa33076ebeb7fd4068238e</id>
<content type='text'>
_idatui_head_row ran 'import ida_bytes/ida_lines/ida_name' on every LISTING
ROW -- 25,500 sys.modules lookups per 500-row page. 50 such imports across
the file, all pointless: this module is never imported by the client
(codemode_client reads it as TEXT and installs it in the database process),
so the no-IDA house rule that forces function-local imports elsewhere does
not apply here.

Measured honestly, it is worth about 3%: heads(500) best-of-40 goes
9.37ms -&gt; 9.07ms, median 9.73 -&gt; 9.65. A microbenchmark of the bare import
predicted 3.2ms/page and was 10x optimistic -- the A/B is what counts, and
it is a small win, not the big one.

ida_hexrays deliberately stays function-local: it is licence-dependent, and
hoisting it would break the whole remote library for someone without the
decompiler rather than failing only when they decompile. Verified by AST
that every ida_* reference still resolves.

Adds experiments/profile_remote.py, which ships cProfile INTO the database
process -- the only way to see the split between IDA's own calls and our
python, which no client-side timer can show.

Full gate: 1042 passed.
</content>
</entry>
</feed>
