mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
480 lines
18 KiB
Python
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)
|