252 lines
9.5 KiB
Python
Executable File
252 lines
9.5 KiB
Python
Executable File
#!/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()
|