import logging import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib, Pango from cht.config import APP_NAME from cht.ui.monitor import MonitorWidget from cht.stream.manager import StreamManager log = logging.getLogger(__name__) class ChtWindow(Adw.ApplicationWindow): def __init__(self, **kwargs): super().__init__(**kwargs) self.set_title(APP_NAME) self.set_default_size(1400, 900) self._streaming = False # Main horizontal paned: agent output (left) | right panels self._main_paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) self._main_paned.set_shrink_start_child(False) self._main_paned.set_shrink_end_child(False) self._main_paned.set_position(450) # Left: Agent output panel self._agent_output = self._build_agent_output() self._main_paned.set_start_child(self._agent_output) # Right: vertical stack of panels right_box = self._build_right_panels() self._main_paned.set_end_child(right_box) # Wrap in toolbar view with header toolbar = Adw.ToolbarView() header = Adw.HeaderBar() header.set_title_widget(Gtk.Label(label=APP_NAME)) self._connect_btn = Gtk.Button(label="Connect") self._connect_btn.add_css_class("suggested-action") self._connect_btn.connect("clicked", self._on_connect_clicked) header.pack_start(self._connect_btn) toolbar.add_top_bar(header) toolbar.set_content(self._main_paned) self.set_content(toolbar) # Stream manager self._stream_mgr = None # Connect window close to cleanup self.connect("close-request", self._on_close) log.info("Window initialized") def _on_connect_clicked(self, button): if self._streaming: self._stop_stream() else: self._start_stream() def _start_stream(self): log.info("Starting stream...") self._connect_btn.set_label("Disconnect") self._connect_btn.remove_css_class("suggested-action") self._connect_btn.add_css_class("destructive-action") self._streaming = True self._stream_mgr = StreamManager() log.info("Session: %s", self._stream_mgr.session_id) log.info("Session dir: %s", self._stream_mgr.session_dir) fifo_path = self._stream_mgr.start_recorder_with_monitor() log.info("FIFO path: %s", fifo_path) log.info("Stream URL: %s", self._stream_mgr.stream_url) log.info("Recorder started, waiting for sender connection...") self._monitor.start_mpv(fifo_path) log.info("Monitor (mpv) started") self._stream_mgr.start_frame_extractor() log.info("Frame extractor started") def _stop_stream(self): log.info("Stopping stream...") self._monitor.stop() log.info("Monitor stopped") if self._stream_mgr: self._stream_mgr.stop_all() log.info("Stream manager stopped") self._stream_mgr = None self._connect_btn.set_label("Connect") self._connect_btn.remove_css_class("destructive-action") self._connect_btn.add_css_class("suggested-action") self._streaming = False log.info("Stream stopped, ready to reconnect") def _on_close(self, *args): log.info("Window closing, cleaning up...") self._monitor.stop() if self._stream_mgr: self._stream_mgr.stop_all() def _build_agent_output(self): """Left panel: agent output log.""" box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) label = Gtk.Label(label="Agent Output") label.add_css_class("heading") label.set_margin_top(8) label.set_margin_bottom(8) box.append(label) self._agent_output_view = Gtk.TextView() self._agent_output_view.set_editable(False) self._agent_output_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self._agent_output_view.set_cursor_visible(False) self._agent_output_view.set_left_margin(8) self._agent_output_view.set_right_margin(8) self._agent_output_view.set_top_margin(4) self._agent_output_view.set_bottom_margin(4) scroll = Gtk.ScrolledWindow() scroll.set_vexpand(True) scroll.set_child(self._agent_output_view) box.append(scroll) frame = Gtk.Frame() frame.set_child(box) return frame def _build_right_panels(self): right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) top_paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) top_paned.set_shrink_start_child(False) top_paned.set_shrink_end_child(False) self._monitor = MonitorWidget() self._monitor.set_hexpand(True) stream_frame = Gtk.Frame() stream_frame.set_child(self._monitor) top_paned.set_start_child(stream_frame) self._waveform_area = self._build_panel("Waveform", height=250, width=200) top_paned.set_end_child(self._waveform_area) top_paned.set_position(650) right_box.append(top_paned) self._frames_panel = self._build_frames_panel() right_box.append(self._frames_panel) self._transcript_panel = self._build_transcript_panel() right_box.append(self._transcript_panel) self._agent_input = self._build_agent_input() right_box.append(self._agent_input) return right_box def _build_panel(self, title, height=200, width=-1): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) label = Gtk.Label(label=title) label.add_css_class("heading") label.set_margin_top(4) label.set_margin_bottom(4) box.append(label) area = Gtk.DrawingArea() area.set_content_height(height) if width > 0: area.set_content_width(width) area.set_vexpand(False) area.set_hexpand(True) box.append(area) frame = Gtk.Frame() frame.set_child(box) return frame def _build_frames_panel(self): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) label = Gtk.Label(label="Frames Extracted") label.add_css_class("heading") label.set_margin_top(4) label.set_margin_bottom(4) box.append(label) self._frames_flow = Gtk.FlowBox() self._frames_flow.set_orientation(Gtk.Orientation.HORIZONTAL) self._frames_flow.set_max_children_per_line(20) self._frames_flow.set_min_children_per_line(1) self._frames_flow.set_selection_mode(Gtk.SelectionMode.MULTIPLE) self._frames_flow.set_homogeneous(True) scroll = Gtk.ScrolledWindow() scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scroll.set_min_content_height(120) scroll.set_child(self._frames_flow) box.append(scroll) frame = Gtk.Frame() frame.set_child(box) return frame def _build_transcript_panel(self): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) label = Gtk.Label(label="Transcript") label.add_css_class("heading") label.set_margin_top(4) label.set_margin_bottom(4) box.append(label) self._transcript_view = Gtk.TextView() self._transcript_view.set_editable(False) self._transcript_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self._transcript_view.set_cursor_visible(False) self._transcript_view.set_left_margin(8) self._transcript_view.set_right_margin(8) scroll = Gtk.ScrolledWindow() scroll.set_vexpand(True) scroll.set_min_content_height(150) scroll.set_child(self._transcript_view) box.append(scroll) frame = Gtk.Frame() frame.set_child(box) return frame def _build_agent_input(self): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) box.set_margin_start(4) box.set_margin_end(4) box.set_margin_top(4) box.set_margin_bottom(4) self._input_entry = Gtk.Entry() self._input_entry.set_hexpand(True) self._input_entry.set_placeholder_text("Message agent... (use @ to reference frames/transcripts)") self._input_entry.connect("activate", self._on_input_activate) box.append(self._input_entry) send_btn = Gtk.Button(label="Send") send_btn.add_css_class("suggested-action") send_btn.connect("clicked", self._on_send_clicked) box.append(send_btn) frame = Gtk.Frame() frame.set_child(box) return frame def _on_input_activate(self, entry): self._send_message() def _on_send_clicked(self, button): self._send_message() def _send_message(self): text = self._input_entry.get_text().strip() if not text: return buf = self._agent_output_view.get_buffer() end_iter = buf.get_end_iter() buf.insert(end_iter, f"\n> {text}\n") self._input_entry.set_text("") def append_agent_output(self, text): buf = self._agent_output_view.get_buffer() end_iter = buf.get_end_iter() buf.insert(end_iter, text + "\n") def append_transcript(self, entry_id, text): buf = self._transcript_view.get_buffer() end_iter = buf.get_end_iter() buf.insert(end_iter, f"[{entry_id}] {text}\n") def add_frame_thumbnail(self, frame_id, pixbuf): img = Gtk.Image.new_from_pixbuf(pixbuf) overlay = Gtk.Overlay() overlay.set_child(img) label = Gtk.Label(label=frame_id) label.set_halign(Gtk.Align.START) label.set_valign(Gtk.Align.END) label.add_css_class("caption") overlay.add_overlay(label) self._frames_flow.append(overlay)