Files
mitus/cht/app.py
2026-04-10 18:29:58 -03:00

123 lines
4.3 KiB
Python

import argparse
import logging
import os
import signal
import sys
import threading
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Gdk, Adw, Gio, GLib
from cht.config import APP_ID, APP_NAME
from cht.window import ChtWindow
class ChtApp(Adw.Application):
def __init__(self):
super().__init__(
application_id=APP_ID,
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
)
# Let GLib handle SIGINT/SIGTERM so Ctrl+C triggers graceful shutdown
GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGINT, self._on_signal)
GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, self._on_signal)
def _on_signal(self):
log = logging.getLogger("cht")
log.info("Signal received — shutting down gracefully")
self.quit()
return GLib.SOURCE_REMOVE
def do_shutdown(self):
# Ensure all windows tear down before the process exits
for win in self.get_windows():
if hasattr(win, "teardown"):
win.teardown()
Adw.Application.do_shutdown(self)
def do_activate(self):
win = self.props.active_window
if not win:
css = Gtk.CssProvider()
css.load_from_string(
".frame-selected { outline: 3px solid @accent_color; outline-offset: -3px; border-radius: 6px; }\n"
"row.frame-selected, row.frame-selected:hover { background: alpha(@accent_color, 0.25); outline: none; border-radius: 0; }\n"
".frame-highlight { outline: 2px solid @warning_color; outline-offset: -2px; border-radius: 4px; opacity: 0.9; }\n"
"row.frame-highlight { background: alpha(@warning_color, 0.15); }"
)
Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(),
css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
win = ChtWindow(application=self)
win.present()
# Auto-connect for E2E testing: --auto-connect
# Delay gives the GUI time to fully render before starting the stream.
if os.environ.get("_CHT_AUTO_CONNECT") == "1" and not win._lifecycle.is_streaming:
GLib.timeout_add(2000, lambda: win._on_connect_clicked(None) or False)
_STDERR_SKIP = [b"eglExportDMABUFImage"]
def _filter_stderr():
"""Redirect fd 2 through a pipe; drop lines matching _STDERR_SKIP."""
real_stderr_fd = os.dup(2)
real_stderr = os.fdopen(real_stderr_fd, "wb", buffering=0)
r_fd, w_fd = os.pipe()
os.dup2(w_fd, 2)
os.close(w_fd)
def _pump():
with os.fdopen(r_fd, "rb", buffering=0) as pipe:
buf = b""
while True:
chunk = pipe.read(4096)
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
if line.strip() and not any(skip in line for skip in _STDERR_SKIP):
real_stderr.write(line + b"\n")
real_stderr.flush()
if buf:
real_stderr.write(buf)
real_stderr.flush()
t = threading.Thread(target=_pump, daemon=True, name="stderr_filter")
t.start()
def main():
parser = argparse.ArgumentParser(description="CHT — Stream Viewer + Agent")
parser.add_argument("--auto-connect", action="store_true", help="Connect on startup")
parser.add_argument("--python", action="store_true", help="Use Python transport (default)")
parser.add_argument("--rust", action="store_true", help="Use Rust transport")
args, gtk_args = parser.parse_known_args()
# Store parsed options so do_activate can read them
os.environ["_CHT_AUTO_CONNECT"] = "1" if args.auto_connect else "0"
os.environ["_CHT_RUST_TRANSPORT"] = "1" if args.rust else "0"
_filter_stderr()
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("cht")
log.info("CHT starting (transport=%s, auto_connect=%s)",
"rust" if args.rust else "python", args.auto_connect)
app = ChtApp()
return app.run([sys.argv[0]] + gtk_args)
if __name__ == "__main__":
main()