aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/highlight.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 12:34:32 +0200
committerblasty <blasty@local>2026-07-09 12:34:32 +0200
commita6a1aae9cbd92be269840da9ca62b71d6af827b0 (patch)
treec2b36a2d16623aeab23be52bbbdf25110b53ecbe /idatui/highlight.py
parentdecompiler view: Tab/Shift+Tab toggle disasm<->pseudocode, full bodies (diff)
downloadida-tui-a6a1aae9cbd92be269840da9ca62b71d6af827b0.tar.gz
ida-tui-a6a1aae9cbd92be269840da9ca62b71d6af827b0.tar.xz
ida-tui-a6a1aae9cbd92be269840da9ca62b71d6af827b0.zip
fix decompiler highlighting + cursor jank
Highlighting: Textual has NO C/C++ tree-sitter grammar (and the syntax extra wasn't installed), so language='cpp' was a silent no-op. Replace TextArea with a virtualized, Pygments-highlighted ScrollView: - idatui/highlight.py: CLexer -> Rich styles, per-line Segment lists - DecompView: lines highlighted ONCE at load, cached as Strips; O(1) scroll Jank: profiling showed our code is ~3.6ms/move (render_line 0.018ms) -- the cost was terminal repaint volume, since every cursor move refreshed the whole ~43-line viewport. Now: - cursor reactive repaint=False (no implicit full refresh) - region-limited refresh: in-place moves repaint only the 2 changed rows (measured 43 -> 2 render_line calls/move), full refresh only on scroll Applied to both DisasmView and DecompView. pilot suite 15/15 (adds highlighting assertion).
Diffstat (limited to 'idatui/highlight.py')
-rw-r--r--idatui/highlight.py58
1 files changed, 58 insertions, 0 deletions
diff --git a/idatui/highlight.py b/idatui/highlight.py
new file mode 100644
index 0000000..9fc692f
--- /dev/null
+++ b/idatui/highlight.py
@@ -0,0 +1,58 @@
+"""Pygments-based C highlighting for Hex-Rays pseudocode.
+
+Textual's TextArea has no C/C++ tree-sitter grammar (its bundled languages are
+python/rust/go/... only), so ``language="cpp"`` silently does nothing. Pygments
+(already a Rich/Textual dependency) has a solid C lexer, so we tokenize once and
+map tokens to Rich styles, producing per-line Segment lists the virtualized view
+can cache and paint instantly.
+"""
+
+from __future__ import annotations
+
+from rich.segment import Segment
+from rich.style import Style
+from pygments.lexers import CLexer
+from pygments.token import Token
+
+# Token -> style, checked in priority order (first hierarchical match wins).
+# Palette roughly follows a dark IDE theme.
+_STYLES: list[tuple[object, Style]] = [
+ (Token.Comment, Style(color="grey54", italic=True)),
+ (Token.Keyword.Type, Style(color="#4ec9b0")), # __int64, char, ...
+ (Token.Keyword, Style(color="#c586c0", bold=True)), # if/else/return/goto
+ (Token.Name.Builtin, Style(color="#4ec9b0")),
+ (Token.Literal.String, Style(color="#ce9178")), # "..."
+ (Token.Literal.Number, Style(color="#b5cea8")), # 0x10, 42
+ (Token.Operator, Style(color="#d4d4d4")),
+ (Token.Punctuation, Style(color="grey70")),
+ (Token.Name, Style(color="#dcdcaa")), # identifiers / calls
+]
+_DEFAULT = Style(color="#d4d4d4")
+
+_lexer = CLexer(stripnl=False, ensurenl=False)
+
+
+def _style_for(token) -> Style:
+ for ttype, style in _STYLES:
+ if token in ttype:
+ return style
+ return _DEFAULT
+
+
+def highlight_c(code: str) -> list[list[Segment]]:
+ """Return one list of Segments per source line (no trailing newline segs)."""
+ lines: list[list[Segment]] = [[]]
+ for token, value in _lexer.get_tokens(code):
+ if not value:
+ continue
+ style = _style_for(token)
+ parts = value.split("\n")
+ for i, part in enumerate(parts):
+ if i > 0:
+ lines.append([])
+ if part:
+ lines[-1].append(Segment(part, style))
+ # get_tokens tends to append a final empty line; drop a lone trailing blank.
+ if len(lines) > 1 and not lines[-1]:
+ lines.pop()
+ return lines