splinter-keep/tools/run_widgets.py
2026-07-03 21:26:24 +02:00

135 lines
3.6 KiB
Python

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}")