diff --git a/server.py b/server.py index c6cd1ea..d7353fb 100755 --- a/server.py +++ b/server.py @@ -253,8 +253,10 @@ HTML = HTML.replace("{max_budget}", str(int(config.BUDGET_S / 60))) if __name__ == "__main__": from devices import start_timer + from tui import start_tui port = int(__import__("os").environ.get("PORT", "10000")) start_timer() + start_tui(config.TICK_INTERVAL) server = HTTPServer(("0.0.0.0", port), Handler) print(f"๐Ÿš€ Open http://localhost:{port}") print(f"โฑ๏ธ Budget timer running (checks every {config.TICK_INTERVAL}s, reset at 7:00 AM)") diff --git a/tui.py b/tui.py new file mode 100644 index 0000000..4b5d3ec --- /dev/null +++ b/tui.py @@ -0,0 +1,120 @@ +"""Terminal TUI for device status monitoring.""" + +import os +import sys +import threading +import time + +from devices import status_all, is_curfew_allowed + + +# ANSI escape codes +CLEAR = "\033[2J\033[H" +BOLD = "\033[1m" +RESET = "\033[0m" +GREEN = "\033[92m" +RED = "\033[91m" +YELLOW = "\033[93m" +CYAN = "\033[96m" +GRAY = "\033[90m" + + +def _get_terminal_size(): + """Get terminal width and height.""" + try: + return os.get_terminal_size() + except OSError: + return (80, 24) + + +def _color(text: str, color_code: str) -> str: + """Wrap text in ANSI color code.""" + return f"{color_code}{text}{RESET}" + + +def _separator(width: int = 60) -> str: + """Create a separator line.""" + return "โ”€" * width + + +def render(devices_status, curfew, refresh_interval: int = 10): + """Render the TUI screen.""" + width, height = _get_terminal_size() + lines = [] + + # Header + lines.append(_color("โšก KIDS DEVICES โ€” STATUS", BOLD)) + lines.append("") + + # Curfew status + if curfew["in_curfew"]: + curfew_icon = _color("๐Ÿ”ด BLOCKED", RED) + else: + curfew_icon = _color("๐ŸŸข ALLOWED", GREEN) + lines.append(f" Curfew: {curfew_icon} | {curfew['message']}") + lines.append("") + + # Device list + for device in devices_status: + icon = device["icon"] + name = device["title"] + detail = device["detail"] + + # Parse budget from detail + parts = detail.split(" ยท ") + status_text = parts[0] if parts else "Unknown" + budget_text = parts[1] if len(parts) > 1 else "" + + # Color the status + if "Online" in status_text: + status_color = GREEN + elif "Offline" in status_text: + status_color = GRAY + else: + status_color = YELLOW + + status_colored = _color(status_text, status_color) + + # Color the budget + if "๐Ÿ”ด" in device["icon"]: + budget_color = RED + elif "๐ŸŸ " in device["icon"]: + budget_color = YELLOW + else: + budget_color = CYAN + + budget_colored = _color(budget_text, budget_color) if budget_text else "" + + lines.append(f" {icon} {name:<12} {status_colored} ยท {budget_colored}") + + # Footer with timestamp + now = time.strftime("%H:%M:%S") + lines.append(f" {_color('Last update:', GRAY)} {now} | Refresh: {refresh_interval}s") + + return "\n".join(lines) + + +def run_tui(refresh_interval: int = 10): + """Run the TUI in a loop.""" + # Run in a separate thread to not block the server + while True: + try: + devices_status, curfew = status_all() + screen = render(devices_status, curfew, refresh_interval) + sys.stdout.write(CLEAR + screen) + sys.stdout.flush() + except Exception: + pass # Don't crash TUI on errors + + time.sleep(refresh_interval) + + +def start_tui(refresh_interval: int = 10): + """Start the TUI in a background thread.""" + tui_thread = threading.Thread( + target=run_tui, + args=(refresh_interval,), + daemon=True, + ) + tui_thread.start() + return tui_thread