#!/usr/bin/env python3 """ Natiris TUI – Text User Interface für den Companion Features: - Realtime State Anzeige - Chat Interface (simuliert) - Admin-Befehle - Quick Actions """ import curses import json import os import sys import time from datetime import datetime, timezone PATHS = { "state": os.path.expanduser("~/natiris/core/natiris_full_state.json"), "config": os.path.expanduser("~/natiris/config/character_genesis.json"), "ollama": os.path.expanduser("~/natiris/bridges/ollama_response.json"), "log": os.path.expanduser("~/natiris/admin/admin_log.json"), } class NatirisTUI: def __init__(self, stdscr): self.stdscr = stdscr self.running = True self.mode = "STATE" # STATE, CHAT, ADMIN self.user_input = "" self.log_lines = [] self.last_update = 0 self.state = {} self.response = "" # Init curses.curs_set(0) curses.start_color() curses.use_default_colors() curses.init_pair(1, curses.COLOR_CYAN, -1) curses.init_pair(2, curses.COLOR_GREEN, -1) curses.init_pair(3, curses.COLOR_YELLOW, -1) curses.init_pair(4, curses.COLOR_RED, -1) curses.init_pair(5, curses.COLOR_WHITE, -1) self.stdscr.nodelay(True) # Initial load self.update_state() def update_state(self): try: with open(PATHS["state"]) as f: self.state = json.load(f) except Exception: self.state = {} try: with open(PATHS["ollama"]) as f: resp = json.load(f) self.response = resp.get("response", "")[:120] except Exception: self.response = "" self.last_update = time.time() def draw_header(self): h, w = self.stdscr.getmaxyx() self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD) self.stdscr.addstr(0, 2, " NATIRIS COMPANION v4.x ") self.stdscr.attroff(curses.color_pair(1) | curses.A_BOLD) self.stdscr.addstr(1, 2, f" Uptime: {int(time.time() - os.path.getmtime(PATHS['state']) if os.path.exists(PATHS['state']) else time.time())}s ") self.stdscr.addstr(2, 2, f" Mode: {self.mode} | {datetime.now(timezone.utc).strftime('%H:%M:%S')} ") def draw_state_panel(self, top, left, height, width): box_top = top box_left = left box_height = height box_width = width # Box frame self.stdscr.attron(curses.color_pair(5)) for y in range(box_top, box_top + box_height): self.stdscr.addstr(y, box_left, "│") self.stdscr.addstr(y, box_left + box_width - 1, "│") self.stdscr.addstr(box_top, box_left, "┌") self.stdscr.addstr(box_top, box_left + box_width - 1, "┐") self.stdscr.addstr(box_top + box_height - 1, box_left, "└") self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘") self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2)) self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2)) self.stdscr.attroff(curses.color_pair(5)) # Title self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD) self.stdscr.addstr(box_top, box_left + 2, " CORE STATE ") self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD) state = self.state.get("modules", {}) core = self.state.get("core_state", {}) lines = [ f" Mood: {core.get('mood', '?')}/10", f" Loneliness: {core.get('loneliness', '?')}/10", f" Anxiety: {core.get('anxiety', '?')}/10", f" Bonded: {core.get('bonded_to') or 'none'}", f" Exclusivity: {state.get('Bond', {}).get('exclusivity_active', False)}", f" Tone: {state.get('Expression', {}).get('tone', 'neutral')}", f" Response: {self.response}", ] for i, line in enumerate(lines): self.stdscr.addstr(box_top + 2 + i, box_left + 2, line[:box_width - 4]) def draw_chat_panel(self, top, left, height, width): box_top = top box_left = left box_height = height box_width = width self.stdscr.attron(curses.color_pair(5)) for y in range(box_top, box_top + box_height): self.stdscr.addstr(y, box_left, "│") self.stdscr.addstr(y, box_left + box_width - 1, "│") self.stdscr.addstr(box_top, box_left, "┌") self.stdscr.addstr(box_top, box_left + box_width - 1, "┐") self.stdscr.addstr(box_top + box_height - 1, box_left, "└") self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘") self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2)) self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2)) self.stdscr.attroff(curses.color_pair(5)) self.stdscr.attron(curses.color_pair(3) | curses.A_BOLD) self.stdscr.addstr(box_top, box_left + 2, " CHAT OUTPUT ") self.stdscr.attroff(curses.color_pair(3) | curses.A_BOLD) if self.mode == "CHAT": self.stdscr.addstr(box_top + box_height - 1, box_left + 2, f"> {self.user_input}_") else: self.stdscr.addstr(box_top + box_height - 1, box_left + 2, " [Press 'c' for chat, 'a' for admin] ") def draw_admin_panel(self, top, left, height, width): box_top = top box_left = left box_height = height box_width = width self.stdscr.attron(curses.color_pair(5)) for y in range(box_top, box_top + box_height): self.stdscr.addstr(y, box_left, "│") self.stdscr.addstr(y, box_left + box_width - 1, "│") self.stdscr.addstr(box_top, box_left, "┌") self.stdscr.addstr(box_top, box_left + box_width - 1, "┐") self.stdscr.addstr(box_top + box_height - 1, box_left, "└") self.stdscr.addstr(box_top + box_height - 1, box_left + box_width - 1, "┘") self.stdscr.addstr(box_top, box_left + 1, "─" * (box_width - 2)) self.stdscr.addstr(box_top + box_height - 1, box_left + 1, "─" * (box_width - 2)) self.stdscr.attroff(curses.color_pair(5)) self.stdscr.attron(curses.color_pair(4) | curses.A_BOLD) self.stdscr.addstr(box_top, box_left + 2, " ADMIN ") self.stdscr.attroff(curses.color_pair(4) | curses.A_BOLD) help_lines = [ " Commands:", " s = status", " e = emotion reset", " m = mood set", " t = trust set", " r = bond reset", " d = debug", ] for i, line in enumerate(help_lines): self.stdscr.addstr(box_top + 2 + i, box_left + 2, line[:box_width - 4]) def handle_input(self): try: key = self.stdscr.getch() if key == -1: return if key == ord('q'): self.running = False elif key == ord('c'): self.mode = "CHAT" elif key == ord('a'): self.mode = "ADMIN" elif key == ord('s') and self.mode == "ADMIN": os.system("cd ~/natiris/admin && python3 AdminInterface.py status") self.update_state() elif key == ord('e') and self.mode == "ADMIN": os.system("cd ~/natiris/admin && python3 AdminInterface.py emotion reset") self.update_state() elif key == ord('r') and self.mode == "ADMIN": os.system("cd ~/natiris/admin && python3 AdminInterface.py bond reset") self.update_state() elif key == ord('t') and self.mode == "ADMIN": os.system("cd ~/natiris/admin && python3 AdminInterface.py trust set 7") self.update_state() elif key == ord('m') and self.mode == "ADMIN": os.system("cd ~/natiris/admin && python3 AdminInterface.py mood set 7.5") self.update_state() elif key == curses.KEY_BACKSPACE or key == 127 or key == 8: if self.mode == "CHAT" and self.user_input: self.user_input = self.user_input[:-1] elif key >= 32 and key < 127: if self.mode == "CHAT": self.user_input += chr(key) except Exception: pass def run(self): while self.running: try: # Update every 2s if time.time() - self.last_update > 2: self.update_state() self.stdscr.clear() h, w = self.stdscr.getmaxyx() # Header self.draw_header() # Panels panel_height = (h - 4) // 2 panel_width = w // 2 - 4 self.draw_state_panel(4, 2, panel_height, w // 2) if self.mode == "ADMIN": self.draw_admin_panel(4 + panel_height, 2, panel_height + 2, w // 2) self.draw_chat_panel(4 + panel_height, w // 2 + 2, panel_height + 2, w // 2 - 2) else: self.draw_chat_panel(4, w // 2 + 2, h - 6, w // 2 - 4) self.handle_input() self.stdscr.refresh() except KeyboardInterrupt: self.running = False def main(): try: curses.wrapper(NatirisTUI) except Exception as e: print(f"TUI error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()