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:
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()
|
||||
Reference in New Issue
Block a user