From 61b93538fe6e03f175b907c19c2d196ecd860782 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Mon, 17 Aug 2026 15:21:59 +0200 Subject: Add kitty graphics fallback for Windows support --- idatui/kittygfx.py | 18 ++++++++-- tests/test_kittygfx.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/test_kittygfx.py diff --git a/idatui/kittygfx.py b/idatui/kittygfx.py index e53fba3..6f419ef 100644 --- a/idatui/kittygfx.py +++ b/idatui/kittygfx.py @@ -71,8 +71,16 @@ def log(msg: str) -> None: # Detection # --------------------------------------------------------------------------- # def _query_tty(timeout: float = 2.0) -> bool: - import termios - import tty as ttymod + # ``termios`` and ``/dev/tty`` are POSIX-only. Native Windows terminals + # generally don't expose the synchronous reply channel this probe needs; + # use the ANSI-art splash there instead of making graphics fatal to the + # whole application. IDATUI_KITTY=1 still permits an explicit override. + try: + import termios + import tty as ttymod + except ImportError: + log("supported: tty queries are unavailable on this platform") + return False try: fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY) @@ -136,7 +144,11 @@ def supported() -> bool: _supported = False # pilot tests, pipes, redirected output log("supported: stdout is not a tty") else: - _supported = _query_tty() + try: + _supported = _query_tty() + except Exception as exc: # graphics are optional on every platform + log(f"supported: terminal query failed ({type(exc).__name__}: {exc})") + _supported = False log(f"supported() -> {_supported}") return _supported diff --git a/tests/test_kittygfx.py b/tests/test_kittygfx.py new file mode 100644 index 0000000..74b6bb7 --- /dev/null +++ b/tests/test_kittygfx.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Cross-platform checks for the optional kitty-graphics startup splash. + +Pure: stdlib only, no terminal, Textual, Code Mode, or IDA. +""" +from __future__ import annotations + +import builtins +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +NEEDS_IDA = False + +from idatui import kittygfx # noqa: E402 + +PASS = FAIL = 0 + + +def check(name, ok, detail=""): + global PASS, FAIL + if ok: + PASS += 1 + print(f" ok {name}") + else: + FAIL += 1 + print(f" FAIL {name} {detail}") + + +def t_no_termios_falls_back(): + """Native Windows has no termios; the splash must simply use ANSI art.""" + original_import = builtins.__import__ + + def without_termios(name, *args, **kwargs): + if name == "termios": + raise ModuleNotFoundError("No module named 'termios'") + return original_import(name, *args, **kwargs) + + builtins.__import__ = without_termios + try: + check("missing termios disables graphics", kittygfx._query_tty(0) is False) + except Exception as exc: # the original Windows startup crash + check("missing termios does not escape", False, + f"{type(exc).__name__}: {exc}") + finally: + builtins.__import__ = original_import + + +def t_probe_failure_is_never_fatal(): + """Even an unexpected platform/probe error cannot prevent TUI startup.""" + original_query = kittygfx._query_tty + original_stdout = sys.__stdout__ + original_supported = kittygfx._supported + old_env = os.environ.pop("IDATUI_KITTY", None) + + class Tty: + def isatty(self): + return True + + def broken_query(): + raise RuntimeError("terminal API failed") + + try: + sys.__stdout__ = Tty() + kittygfx._query_tty = broken_query + kittygfx._supported = None + check("probe exception disables graphics", kittygfx.supported() is False) + check("failed result is cached", kittygfx.supported() is False) + except Exception as exc: + check("probe exception does not escape", False, + f"{type(exc).__name__}: {exc}") + finally: + kittygfx._query_tty = original_query + kittygfx._supported = original_supported + sys.__stdout__ = original_stdout + if old_env is not None: + os.environ["IDATUI_KITTY"] = old_env + + +def main() -> int: + for fn in (t_no_termios_falls_back, t_probe_failure_is_never_fatal): + print(f"\n{fn.__name__}") + fn() + print(f"\n{PASS} passed, {FAIL} failed") + return 1 if FAIL else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) -- cgit v1.3.1-sl0p