Files
MyClub/scripts/browser_terminal_tamagotchi.py
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

480 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Browser-based Terminal Tamagotchi
Terminal interface in browser with real-time updates
"""
import os
import time
import random
import subprocess
from datetime import datetime
from flask import Flask, render_template, jsonify
from flask_socketio import SocketIO, emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'terminal-tamagotchi'
socketio = SocketIO(app, cors_allowed_origins="*")
class BrowserTerminalTamagotchi:
def __init__(self, project_name="MyClub"):
self.project_name = project_name
self.stats_file = os.path.join(os.path.dirname(__file__), 'stats.md')
self.stats = {}
self.last_check = time.time()
self.typing_detected = False
self.recent_changes = 0
# Pet state
self.pet = {
'name': project_name.lower() + '-chi',
'mood': 'content',
'evolution': 'baby',
'level': 1,
'energy': 100,
'hunger': 0,
'coding': 0,
'face': '(o.o)',
'activity': 'idle'
}
# ASCII faces
self.faces = {
'baby': {
'content': ['(o.o)', '(O.o)', '(o.O)'],
'coding': ['(>.<)', '(>_<)', '(>.>_)'],
'happy': ['(^.^)', '(^.^)', '(^o^)'],
'tired': ['(-_-)', '(=_=)', '(x_x)']
},
'child': {
'content': ['(o.o)', '(^.^)', '(•.•)'],
'coding': ['(>_<)', '(>o<)', '(x.x)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'teen': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'adult': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'master': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
}
}
def get_git_status(self):
"""Get git repository status"""
try:
project_root = os.path.dirname(os.path.dirname(self.stats_file))
# Check if we're in a git repository
git_dir = os.path.join(project_root, '.git')
if not os.path.exists(git_dir):
return {'status': 'no_repo', 'branch': None, 'changes': []}
# Try different git status commands to get all files
commands = [
['git', 'status', '--porcelain', '--ignored=traditional'],
['git', 'status', '--porcelain', '--ignored'],
['git', 'status', '--porcelain']
]
result = None
for cmd in commands:
try:
result = subprocess.run(
cmd,
cwd=project_root,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
break
except:
continue
if not result or result.returncode != 0:
return {'status': 'error', 'branch': None, 'changes': []}
# Get current branch
branch_result = subprocess.run(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
cwd=project_root,
capture_output=True,
text=True,
timeout=5
)
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else 'unknown'
# Parse git status output
changes = []
for line in result.stdout.strip().split('\n'):
if line and len(line) >= 3:
status_code = line[:2]
file_path = line[3:]
if status_code == '??':
change_type = 'untracked'
elif status_code == '!!':
change_type = 'ignored'
elif status_code == ' M':
change_type = 'modified'
elif status_code == 'A':
change_type = 'added'
elif status_code == 'D':
change_type = 'deleted'
elif status_code[0] in ['M', 'A', 'D']:
change_type = 'staged'
else:
change_type = 'changed'
changes.append({
'file': file_path,
'type': change_type,
'status': status_code
})
# Debug: print what we found
print(f"Git status found {len(changes)} changes")
return {
'status': 'clean' if not changes else 'dirty',
'branch': current_branch,
'changes': changes,
'total_changes': len(changes)
}
except Exception as e:
print(f"Git status error: {e}") # Debug output
return {'status': 'error', 'branch': None, 'changes': []}
def scan_project_files(self):
"""Scan project files - using exact same logic as project_stats.py"""
try:
project_root = os.path.dirname(os.path.dirname(self.stats_file))
current_time = time.time()
# Use exact same constants as project_stats.py + tamagotchi_env
IGNORED_DIRS = {
'.git',
'.git_backup',
'__pycache__',
'node_modules',
'dist',
'build',
'.next',
'.cache',
'.venv',
'venv',
'tamagotchi_env', # Exclude tamagotchi virtual environment
}
ALLOWED_EXTS = {
'.tsx',
'.css',
'.go',
'.ts',
'.js',
'.html',
'.sql',
'.py',
}
file_stats = {
'directories': 0,
'files': 0,
'lines': 0,
'extensions': {},
'recent_changes': 0,
'current_file': None, # Track most recently modified file
'recent_files': [] # List of recently modified files
}
for current_dir, dirs, files in os.walk(project_root):
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
# Only count directories that are NOT the root directory (same as project_stats.py)
if current_dir != project_root:
file_stats['directories'] += 1
for name in files:
ext = os.path.splitext(name)[1]
if ext not in ALLOWED_EXTS:
continue
file_stats['files'] += 1
path = os.path.join(current_dir, name)
ext_key = ext or '<no_ext>'
line_count = 0
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for _ in f:
line_count += 1
except Exception:
line_count = 0
file_stats['lines'] += line_count
if ext_key not in file_stats['extensions']:
file_stats['extensions'][ext_key] = {'files': 0, 'lines': 0}
file_stats['extensions'][ext_key]['files'] += 1
file_stats['extensions'][ext_key]['lines'] += line_count
# Check for recent changes (last 30 seconds)
try:
mtime = os.path.getmtime(path)
if current_time - mtime < 30:
file_stats['recent_changes'] += 1
# Track the most recently modified file
rel_path = os.path.relpath(path, project_root)
file_info = {
'path': rel_path,
'name': name,
'ext': ext,
'mtime': mtime,
'size': os.path.getsize(path)
}
# Update current file (most recent)
if not file_stats['current_file'] or mtime > file_stats['current_file']['mtime']:
file_stats['current_file'] = file_info
# Add to recent files list (keep last 5)
file_stats['recent_files'].append(file_info)
file_stats['recent_files'] = sorted(
file_stats['recent_files'],
key=lambda x: x['mtime'],
reverse=True
)[:5]
# Debug output
print(f"Recent change detected: {rel_path} ({ext})")
except:
pass
return file_stats
except Exception as e:
return {'directories': 0, 'files': 0, 'lines': 0, 'extensions': {}, 'recent_changes': 0}
def update_pet_state(self, file_stats):
"""Update pet based on file stats"""
self.recent_changes = file_stats['recent_changes']
self.typing_detected = self.recent_changes > 0
lines = file_stats['lines']
if lines > 200000:
self.pet['evolution'] = 'master'
elif lines > 100000:
self.pet['evolution'] = 'adult'
elif lines > 50000:
self.pet['evolution'] = 'teen'
elif lines > 10000:
self.pet['evolution'] = 'child'
else:
self.pet['evolution'] = 'baby'
self.pet['level'] = 1 + (file_stats['files'] // 50) + (lines // 10000)
if self.typing_detected:
self.pet['mood'] = 'coding'
self.pet['activity'] = 'coding'
self.pet['coding'] = min(100, self.pet['coding'] + self.recent_changes * 10)
self.pet['energy'] = min(100, self.pet['energy'] + 5)
else:
self.pet['activity'] = 'idle'
self.pet['coding'] = max(0, self.pet['coding'] - 2)
if self.pet['energy'] > 70:
self.pet['mood'] = 'content'
elif self.pet['energy'] > 40:
self.pet['mood'] = 'tired'
else:
self.pet['mood'] = 'tired'
self.pet['hunger'] = min(100, self.pet['hunger'] + 1)
self.pet['energy'] = max(0, self.pet['energy'] - 1)
evolution_faces = self.faces.get(self.pet['evolution'], self.faces['baby'])
mood_faces = evolution_faces.get(self.pet['mood'], evolution_faces['content'])
self.pet['face'] = random.choice(mood_faces)
def get_terminal_output(self):
"""Generate ultra-compact terminal-style output"""
lines = []
# Header
lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] {self.pet['name'].upper()} TERMINAL")
lines.append("=" * 50)
# Pet and status on one line
lines.append(f"[{self.pet['face']}] {self.pet['mood'].upper()} | LVL:{self.pet['level']} | {self.pet['evolution'].upper()}")
# Status bars on one line
energy_bar = "#" * int(self.pet['energy']//10) + " " * (10 - int(self.pet['energy']//10))
hunger_bar = "#" * int(self.pet['hunger']//10) + " " * (10 - int(self.pet['hunger']//10))
coding_bar = "#" * int(self.pet['coding']//10) + " " * (10 - int(self.pet['coding']//10))
lines.append(f"E:[{energy_bar}] H:[{hunger_bar}] C:[{coding_bar}]")
# Current activity (compact)
if self.typing_detected and self.stats.get('current_file'):
current_file = self.stats['current_file']
file_name = current_file['path'].split('/')[-1]
lines.append(f"EDITING: {file_name} ({current_file['ext']})")
elif self.typing_detected:
lines.append(f"ACTIVITY: {self.recent_changes} files changed")
else:
lines.append(f"Activity: {self.pet['activity'].upper()}")
# Git status (compact)
git_status = self.get_git_status()
if git_status['status'] == 'no_repo':
lines.append("GIT: Not a git repository")
elif git_status['status'] == 'error':
lines.append("GIT: Error checking git")
elif git_status['status'] == 'clean':
lines.append(f"GIT: Clean ({git_status['branch']})")
else:
untracked = len([c for c in git_status['changes'] if c['type'] == 'untracked'])
modified = len([c for c in git_status['changes'] if c['type'] == 'modified'])
staged = len([c for c in git_status['changes'] if c['type'] == 'staged'])
deleted = len([c for c in git_status['changes'] if c['type'] == 'deleted'])
ignored = len([c for c in git_status['changes'] if c['type'] == 'ignored'])
lines.append(f"GIT: {git_status['total_changes']} files ({git_status['branch']})")
lines.append(f" U:{untracked} M:{modified} S:{staged} D:{deleted} I:{ignored}")
# Recent files (compact)
if self.stats.get('recent_files'):
recent_names = [f['path'].split('/')[-1] for f in self.stats['recent_files'][:2]]
lines.append(f"RECENT: {', '.join(recent_names)}")
# Project stats (compact)
lines.append(f"PROJECT: {self.stats.get('files', 0)} files | {self.stats.get('lines', 0):,} lines")
# Top languages (compact)
extensions = self.stats.get('extensions', {})
if extensions:
sorted_exts = sorted(extensions.items(), key=lambda x: x[1]['lines'], reverse=True)[:3]
top_langs = [f"{ext}({data['files']})" for ext, data in sorted_exts]
lines.append(f"LANGUAGES: {', '.join(top_langs)}")
lines.append("CONTROLS: [f]eed [p]lay [r]efresh [q]uit")
lines.append("=" * 50)
return "\n".join(lines)
def feed_pet(self):
"""Feed the pet"""
self.pet['hunger'] = max(0, self.pet['hunger'] - 30)
self.pet['energy'] = min(100, self.pet['energy'] + 10)
self.pet['mood'] = 'content'
def play_with_pet(self):
"""Play with pet"""
self.pet['energy'] = max(0, self.pet['energy'] - 10)
self.pet['mood'] = 'content'
self.pet['activity'] = 'playing'
# Global instance
tamagotchi = None
@app.route('/')
def index():
"""Main terminal page"""
return render_template('terminal.html')
@app.route('/api/terminal-data')
def get_terminal_data():
"""Get terminal output as JSON"""
return jsonify({
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
@socketio.on('connect')
def handle_connect():
"""Handle client connection"""
emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
@socketio.on('command')
def handle_command(data):
"""Handle terminal commands"""
cmd = data.get('command', '').lower().strip()
if cmd == 'f':
tamagotchi.feed_pet()
print(f"Feed command received - Hunger: {tamagotchi.pet['hunger']}%")
elif cmd == 'p':
tamagotchi.play_with_pet()
print(f"Play command received - Energy: {tamagotchi.pet['energy']}%")
elif cmd == 'r':
print("Refresh command received - stats refresh automatically")
elif cmd == 'q':
emit('quit', {'message': 'Terminal session ended'})
return
# Send updated state
emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
def background_monitor():
"""Background thread to monitor files and broadcast updates"""
while True:
try:
# Scan files and update pet
file_stats = tamagotchi.scan_project_files()
tamagotchi.stats = file_stats
tamagotchi.update_pet_state(file_stats)
# Broadcast update to all clients
socketio.emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
except Exception as e:
print(f"Monitor error: {e}")
time.sleep(2) # Update every 2 seconds
if __name__ == '__main__':
# Initialize tamagotchi
project_name = "MyClub"
tamagotchi = BrowserTerminalTamagotchi(project_name)
# Start background monitoring
import threading
monitor_thread = threading.Thread(target=background_monitor, daemon=True)
monitor_thread.start()
print("🖥️ Browser Terminal Tamagotchi Starting...")
print("🌐 Open your browser to: http://localhost:5000")
print("⌨️ Terminal interface in browser!")
print("🔥 Real-time file monitoring!")
socketio.run(app, host='0.0.0.0', port=5000, debug=False)