"""Tree-style display of MPEG-2 PSI structure (PAT/PMT). Renders a hierarchical view of the Program Association Table and its child Program Map Tables using Rich markup inside a Static widget. Shows transport stream ID, program numbers, PMT PIDs, PCR PIDs, and elementary stream types with their PIDs. """ from textual.widget import Widget from textual.widgets import Static from textual.app import ComposeResult class PsiTree(Widget): """Hierarchical PAT/PMT display for transport stream program structure.""" DEFAULT_CSS = """ PsiTree { height: 1fr; min-height: 8; background: #0e1420; border: round #1a2a3a; padding: 1; overflow-y: auto; } """ def __init__(self, **kwargs): super().__init__(**kwargs) self._pat: dict | None = None self._pmts: dict[int, dict] = {} def compose(self) -> ComposeResult: yield Static( "[#506878]Waiting for PSI data...[/]", id="psi-content" ) def update_pat(self, pat: dict) -> None: """Update the Program Association Table and redraw.""" self._pat = pat self._refresh() def update_pmt(self, pmt_pid: int, pmt: dict) -> None: """Update a Program Map Table and redraw.""" self._pmts[pmt_pid] = pmt self._refresh() def clear_tree(self) -> None: """Reset all PSI state and show placeholder.""" self._pat = None self._pmts.clear() if self.is_mounted: self.query_one("#psi-content", Static).update( "[#506878]Waiting for PSI data...[/]" ) def _refresh(self) -> None: """Rebuild the tree markup from current PAT/PMT data.""" if not self.is_mounted: return lines: list[str] = [] if self._pat: tsid = self._pat.get("transport_stream_id", 0) ver = self._pat.get("version", 0) lines.append( f"[bold #00d4aa]PAT[/] [#506878]TSID=0x{tsid:04X} v{ver}[/]" ) programs = self._pat.get("programs", {}) # Separate NIT (program 0) from real programs real_progs = {k: v for k, v in programs.items() if k != 0} for prog_num, pmt_pid in sorted(programs.items()): if prog_num == 0: lines.append( f" [#7090a8]\u251c\u2500[/] [#506878]NIT[/] " f"PID=0x{pmt_pid:04X}" ) else: is_last = prog_num == max(real_progs.keys()) prefix = "\u2514\u2500" if is_last else "\u251c\u2500" lines.append( f" [#7090a8]{prefix}[/] " f"[bold #c8d0d8]Program {prog_num}[/] " f"PMT=0x{pmt_pid:04X}" ) # Expand PMT details if available if pmt_pid in self._pmts: pmt = self._pmts[pmt_pid] pcr_pid = pmt.get("pcr_pid", 0) indent = " " if is_last else "\u2502 " lines.append( f" {indent} [#506878]PCR PID=0x{pcr_pid:04X}[/]" ) streams = pmt.get("streams", []) for j, s in enumerate(streams): s_last = j == len(streams) - 1 s_prefix = "\u2514\u2500" if s_last else "\u251c\u2500" type_name = s.get("type_name", "Unknown") epid = s.get("elementary_pid", 0) lines.append( f" {indent} [#7090a8]{s_prefix}[/] " f"[#c8d0d8]{type_name}[/] " f"PID=0x{epid:04X}" ) else: lines.append("[#506878]No PAT received yet[/]") self.query_one("#psi-content", Static).update("\n".join(lines))