Files
natiris/tui/natiris_tui.py

252 lines
9.5 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <val> = mood set",
" t <val> = 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()