Files
Kao/kao_tui.py
Spencer 50e34b24c6 Bump to v2.2.0: fix warning/bubble sounds, add klaxon, refresh docs
- index.html: fix playWarningSound (440→550 Hz, louder), fix playBubbleSound
  (audible volumes/durations), add looping klaxon sound (sawtooth wah-wah),
  stopKlaxon() on tap and state clear, bump VERSION to v2.2.0
- kao_tui.py: add klaxon to SOUNDS list, drop notify duration 5→2s for
  faster iteration; also include improved post() error reporting
- CLAUDE.md: add kao_tui.py to file structure, fix personality table
  (remove ˙▿˙ row not in aggregator), add klaxon to sound list
- README.md: add klaxon to sound list, update counts
- openapi.yaml: bump version to 2.2.0, add klaxon to sound enum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 17:30:19 -06:00

133 lines
5.9 KiB
Python

"""Kao TUI — developer test tool for firing events, faces, and sounds."""
import sys
import requests
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Footer, Header, ListView, ListItem, Label, TabbedContent, TabPane
BASE_URL = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://localhost:5100"
SOUNDS = [
"chime", "alert", "warning", "critical", "success", "notify",
"doorbell", "knock", "ding", "blip", "siren", "tada",
"ping", "bubble", "fanfare", "alarm", "klaxon", "none",
]
FACES = [
{"emote": "( ^_^)", "animation": "breathing", "color": "#00FF00", "desc": "calm"},
{"emote": "( ˙▿˙)", "animation": "floating", "color": "#00FF00", "desc": "content"},
{"emote": "(◕‿◕)", "animation": "bouncing", "color": "#00FF00", "desc": "cheerful"},
{"emote": "( ・ω・)", "animation": "swaying", "color": "#00FF00", "desc": "curious"},
{"emote": "( ˘▽˘)", "animation": "breathing", "color": "#00FF00", "desc": "cozy"},
{"emote": "( -_^)", "animation": "blink", "color": "#00FF00", "desc": "wink"},
{"emote": "( x_x)", "animation": "shaking", "color": "#FF0000", "desc": "critical"},
{"emote": "( o_o)", "animation": "breathing", "color": "#FFFF00", "desc": "warning"},
{"emote": "( 'o')", "animation": "popping", "color": "#0088FF", "desc": "notify"},
{"emote": r"\(^o^)/", "animation": "celebrating", "color": "#00FF00", "desc": "celebrate"},
{"emote": "( -_-)zzZ", "animation": "sleeping", "color": "#333333", "desc": "sleep"},
{"emote": "( ?.?)", "animation": "searching", "color": "#888888", "desc": "lost"},
]
EVENTS = [
{"label": "Critical (priority 1)", "id": "tui_1", "priority": 1, "message": "TUI Critical"},
{"label": "Warning (priority 2)", "id": "tui_2", "priority": 2, "message": "TUI Warning"},
{"label": "Notify (priority 3)", "id": "tui_3", "priority": 3, "message": "TUI Notify"},
{"label": "── Clear all ──", "id": None},
]
CONTROLS = [
{"label": "Sleep", "action": "sleep"},
{"label": "Wake", "action": "wake"},
{"label": "Clear all events", "action": "clear-all"},
]
def post(path: str, data: dict | None = None) -> tuple[bool, str]:
try:
resp = requests.post(f"{BASE_URL}/{path}", json=data or {}, timeout=5)
return resp.ok, "" if resp.ok else f"HTTP {resp.status_code}: {resp.text[:80]}"
except Exception as e:
return False, str(e)
class KaoTUI(App):
TITLE = f"Kao TUI — {BASE_URL}"
CSS = """
ListView {
height: 1fr;
border: none;
}
ListItem {
padding: 0 2;
}
"""
BINDINGS = [
Binding("q", "quit", "Quit"),
]
def compose(self) -> ComposeResult:
yield Header()
with TabbedContent("Sounds", "Faces", "Events", "Controls"):
with TabPane("Sounds", id="tab-sounds"):
with ListView(id="list-sounds"):
for name in SOUNDS:
yield ListItem(Label(name), id=f"sound-{name}")
with TabPane("Faces", id="tab-faces"):
with ListView(id="list-faces"):
for face in FACES:
label = f"{face['emote']} {face['animation']} {face['desc']}"
yield ListItem(Label(label), id=f"face-{face['desc']}")
with TabPane("Events", id="tab-events"):
with ListView(id="list-events"):
for ev in EVENTS:
yield ListItem(Label(ev["label"]), id=f"event-{ev['id'] or 'clearall'}")
with TabPane("Controls", id="tab-controls"):
with ListView(id="list-controls"):
for ctrl in CONTROLS:
yield ListItem(Label(ctrl["label"]), id=f"ctrl-{ctrl['action']}")
yield Footer()
def on_list_view_selected(self, event: ListView.Selected) -> None:
item_id = event.item.id or ""
if item_id.startswith("sound-"):
name = item_id[len("sound-"):]
ok, err = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 2})
self.notify(f"{name} sent" if ok else f"{name} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("face-"):
desc = item_id[len("face-"):]
face = next((f for f in FACES if f["desc"] == desc), None)
if face:
ok, err = post("notify", {
"emote": face["emote"],
"animation": face["animation"],
"color": face["color"],
"message": f"face: {face['desc']}",
"duration": 2,
})
self.notify(f"{face['emote']} {face['desc']} sent" if ok else f"{face['desc']} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("event-"):
suffix = item_id[len("event-"):]
if suffix == "clearall":
ok, err = post("clear-all")
self.notify("clear-all sent" if ok else f"clear-all FAILED: {err}", severity="information" if ok else "error")
else:
ev = next((e for e in EVENTS if e["id"] == suffix), None)
if ev:
ok, err = post("event", {"id": ev["id"], "priority": ev["priority"], "message": ev["message"], "ttl": 10})
self.notify(f"{ev['label'].strip()} sent" if ok else f"{ev['label'].strip()} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("ctrl-"):
action = item_id[len("ctrl-"):]
ok, err = post(action)
self.notify(f"{action} sent" if ok else f"{action} FAILED: {err}", severity="information" if ok else "error")
if __name__ == "__main__":
KaoTUI().run()