from __future__ import annotations from textual.app import ComposeResult from textual.containers import Vertical from textual.screen import Screen from textual.widgets import Button, Input, Static from rich.markdown import Markdown as RichMarkdown from run_utils import ( CHAR_PATH, TODAY, REFRESH_SECS, clear_llm_log, ensure_log, read_todo, read_log_tail, status_summary, log_count, ) from run_ambience import HAS_AUDIO # module-level ref filled by ChaosTUI app_ambience_player: object | None = None class RollModal(Screen): CSS = """ RollModal { align: center middle; background: rgba(0, 0, 0, 0.75); } #roll-dialog { width: 44; height: auto; padding: 2 3; background: #2a2a3a; border: thick #e0ad4c; } #roll-title { text-style: bold; color: #ffd93d; text-align: center; height: 3; } #roll-reason { color: #c0b090; text-align: center; height: 3; } #roll-input { margin: 1 0; } #roll-submit { width: 100%; } #roll-hint { color: #888888; text-align: center; height: 1; } """ def __init__(self, dice: str, reason: str) -> None: super().__init__() self.dice = dice self.reason = reason def compose(self) -> ComposeResult: with Vertical(id="roll-dialog"): yield Static(f"[bold]🎲 ROLL {self.dice}[/bold]", id="roll-title") yield Static(f"Reason: {self.reason}", id="roll-reason") yield Input(placeholder="Enter the number you rolled...", id="roll-input") yield Button("Submit", id="roll-submit", variant="primary") yield Static("(or press Enter)", id="roll-hint") def on_input_submitted(self, event: Input.Submitted) -> None: self._submit(event.value) def on_button_pressed(self, event: Button.Pressed) -> None: if event.button.id == "roll-submit": self._submit(self.query_one("#roll-input", Input).value) def _submit(self, value: str) -> None: val = value.strip() if val: self.dismiss(val) class AutoStatic(Static): def load(self): raise NotImplementedError def on_mount(self): clear_llm_log() ensure_log() self.load() self.set_interval(REFRESH_SECS, self.load) class TodoPane(AutoStatic): def load(self): items = read_todo() self.update("\n".join(f" ☐ {i}" for i in items)) class TranscriptPane(AutoStatic): def load(self): lines = read_log_tail() display = "\n".join(lines[-80:]) if lines: display += "\n\n>>--- NOW --->" self.update(display) self.call_after_refresh(self._scroll_bottom) def _scroll_bottom(self): if self.parent and hasattr(self.parent, 'scroll_end'): self.parent.scroll_end(animate=False) class CharPane(AutoStatic): def load(self): if not CHAR_PATH.exists(): self.update("*No character sheet*") return self.update(RichMarkdown(CHAR_PATH.read_text().strip())) class StatusBar(AutoStatic): def load(self): char = status_summary() count = log_count() todo = len(read_todo()) music = "" if not HAS_AUDIO: music = " │ ♫ (install miniaudio)" elif app_ambience_player: name = app_ambience_player.ambience_name music = f" │ ♫ {name}" self.update(f"{char} │ {count} entries │ {todo} todo │ {TODAY}{music}")