diff options
| author | blasty <blasty@local> | 2026-07-09 12:34:32 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-09 12:34:32 +0200 |
| commit | a6a1aae9cbd92be269840da9ca62b71d6af827b0 (patch) | |
| tree | c2b36a2d16623aeab23be52bbbdf25110b53ecbe /idatui/highlight.py | |
| parent | decompiler view: Tab/Shift+Tab toggle disasm<->pseudocode, full bodies (diff) | |
| download | ida-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.py | 58 |
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 |
