#!/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 '' 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)