Files
Kao/kao_tui.py
Spencer 92e6441218 Bump to v2.1.0: add kao_tui.py developer TUI and WSL venv
- Add kao_tui.py: Textual TUI with Sounds, Faces, Events, Controls tabs
- Add textual to requirements.txt
- Add venv activation instructions to README
- Add Developer TUI section to README

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

133 lines
5.7 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", "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) -> bool:
try:
resp = requests.post(f"{BASE_URL}/{path}", json=data or {}, timeout=5)
return resp.ok
except Exception:
return False
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 = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 5})
self.notify(f"{name} {'sent' if ok else 'FAILED'}", 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 = post("notify", {
"emote": face["emote"],
"animation": face["animation"],
"color": face["color"],
"message": f"face: {face['desc']}",
"duration": 5,
})
self.notify(f"{face['emote']} {face['desc']} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
elif item_id.startswith("event-"):
suffix = item_id[len("event-"):]
if suffix == "clearall":
ok = post("clear-all")
self.notify(f"clear-all {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
else:
ev = next((e for e in EVENTS if e["id"] == suffix), None)
if ev:
ok = post("event", {"id": ev["id"], "priority": ev["priority"], "message": ev["message"], "ttl": 10})
self.notify(f"{ev['label'].strip()} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
elif item_id.startswith("ctrl-"):
action = item_id[len("ctrl-"):]
ok = post(action)
self.notify(f"{action} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
if __name__ == "__main__":
KaoTUI().run()