Files
mitus/cht/window.py
2026-04-01 13:59:31 -03:00

293 lines
9.8 KiB
Python

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)