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).
|
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
|
## Status Faces
|
||||||
|
|
||||||
| State | Emote | Meaning |
|
| State | Emote | Meaning |
|
||||||
@@ -179,6 +196,23 @@ automation:
|
|||||||
- `animation`: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying`
|
- `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`
|
- `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
|
## API Reference
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
|
|||||||
@@ -217,7 +217,7 @@
|
|||||||
const emoteEl = document.getElementById("emote");
|
const emoteEl = document.getElementById("emote");
|
||||||
const messageEl = document.getElementById("message");
|
const messageEl = document.getElementById("message");
|
||||||
const POLL_INTERVAL = 2000;
|
const POLL_INTERVAL = 2000;
|
||||||
const VERSION = "v2.0.0";
|
const VERSION = "v2.1.0";
|
||||||
|
|
||||||
// Sound system
|
// Sound system
|
||||||
let audioCtx = null;
|
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
|
requests==2.32.5
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
Werkzeug==3.1.5
|
Werkzeug==3.1.5
|
||||||
|
textual
|
||||||
|
|||||||
Reference in New Issue
Block a user