diff --git a/README.md b/README.md index 3914cce..e64c355 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,23 @@ python kao.py Open http://localhost:5100 on your phone (use Fully Kiosk Browser for best results). +## Activating the venv + +Two venvs exist — one for Windows, one for WSL: + +```bash +# WSL / Linux +source venv-wsl/bin/activate + +# Windows (PowerShell) +.\venv\Scripts\Activate.ps1 + +# Windows (cmd) +.\venv\Scripts\activate.bat +``` + +Deactivate either with `deactivate`. + ## Status Faces | State | Emote | Meaning | @@ -179,6 +196,23 @@ automation: - `animation`: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying` - `sound`: `chime`, `alert`, `warning`, `critical`, `success`, `notify`, `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `none` +## Developer TUI + +`kao_tui.py` is a terminal UI for firing test events, faces, and sounds at a running Kao instance — no `curl` needed. + +```bash +python kao_tui.py # connect to http://localhost:5100 +python kao_tui.py http://192.168.1.x:5100 # custom URL +``` + +Four tabs: +- **Sounds** — fire any of the 17 sounds via `/notify` +- **Faces** — send any preset emote/animation combo via `/notify` +- **Events** — post Critical/Warning/Notify events (10s TTL) or clear all +- **Controls** — Sleep, Wake, Clear all + +Navigate with `↑↓` or `Tab`, press `Enter` to fire, `Q` to quit. A toast confirms each action. + ## API Reference | Endpoint | Method | Description | diff --git a/index.html b/index.html index 6a88f15..5ea3845 100644 --- a/index.html +++ b/index.html @@ -217,7 +217,7 @@ const emoteEl = document.getElementById("emote"); const messageEl = document.getElementById("message"); const POLL_INTERVAL = 2000; - const VERSION = "v2.0.0"; + const VERSION = "v2.1.0"; // Sound system let audioCtx = null; diff --git a/kao_tui.py b/kao_tui.py new file mode 100644 index 0000000..1f2b44d --- /dev/null +++ b/kao_tui.py @@ -0,0 +1,132 @@ +"""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() diff --git a/requirements.txt b/requirements.txt index ebbc292..561b142 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ psutil==7.2.2 requests==2.32.5 urllib3==2.6.3 Werkzeug==3.1.5 +textual