- 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>
133 lines
5.7 KiB
Python
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()
|