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:
34
README.md
34
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 |
|
||||
|
||||
@@ -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
132
kao_tui.py
Normal 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()
|
||||
@@ -12,3 +12,4 @@ psutil==7.2.2
|
||||
requests==2.32.5
|
||||
urllib3==2.6.3
|
||||
Werkzeug==3.1.5
|
||||
textual
|
||||
|
||||
Reference in New Issue
Block a user