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>
This commit is contained in:
2026-02-20 17:12:05 -06:00
parent a074a42d40
commit 92e6441218
4 changed files with 168 additions and 1 deletions

View File

@@ -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 |

View File

@@ -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;

132
kao_tui.py Normal file
View File

@@ -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()

View File

@@ -12,3 +12,4 @@ psutil==7.2.2
requests==2.32.5
urllib3==2.6.3
Werkzeug==3.1.5
textual