""" Log Viewer - Real-time log display and monitoring """ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLabel, QCheckBox, QComboBox, QGroupBox, QSplitter, QSpinBox ) from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QTextCursor, QFont from pathlib import Path import datetime import os class LogViewer(QWidget): """Widget for viewing translation logs and monitoring progress.""" log_updated = pyqtSignal(str) def __init__(self): super().__init__() self.log_files = { "Translation History": "log/translationHistory.txt", "Mismatch History": "log/mismatchHistory.txt" } self.auto_refresh_timer = QTimer() self.auto_refresh_timer.timeout.connect(self.refresh_logs) # Performance optimization tracking self.file_mod_times = {} # Track file modification times self.file_positions = {} # Track file read positions for tail-only updates self.max_lines = 1000 # Maximum lines to display self.refresh_interval = 2000 # Default refresh interval in ms self.init_ui() def init_ui(self): """Initialize the user interface.""" layout = QVBoxLayout() # Control panel control_group = QGroupBox("Log Controls") control_layout = QHBoxLayout() # Log file selector self.log_selector = QComboBox() self.log_selector.addItems(list(self.log_files.keys())) self.log_selector.currentTextChanged.connect(self.load_selected_log) control_layout.addWidget(QLabel("Log File:")) control_layout.addWidget(self.log_selector) # Auto-refresh checkbox self.auto_refresh_cb = QCheckBox("Auto Refresh") self.auto_refresh_cb.toggled.connect(self.toggle_auto_refresh) control_layout.addWidget(self.auto_refresh_cb) # Max lines control control_layout.addWidget(QLabel("Max Lines:")) self.max_lines_spinbox = QSpinBox() self.max_lines_spinbox.setRange(100, 10000) self.max_lines_spinbox.setValue(self.max_lines) self.max_lines_spinbox.setSingleStep(100) self.max_lines_spinbox.valueChanged.connect(self.update_max_lines) control_layout.addWidget(self.max_lines_spinbox) # Manual refresh button self.refresh_button = QPushButton("Refresh") self.refresh_button.clicked.connect(self.refresh_logs) control_layout.addWidget(self.refresh_button) # Clear button self.clear_button = QPushButton("Clear") self.clear_button.clicked.connect(self.clear_log) control_layout.addWidget(self.clear_button) control_layout.addStretch() control_group.setLayout(control_layout) layout.addWidget(control_group) # Log display area self.log_display = QTextEdit() self.log_display.setReadOnly(True) self.log_display.setFont(QFont("Consolas", 10)) self.log_display.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #ffffff; border: 1px solid #555555; } """) layout.addWidget(self.log_display) # Status bar self.status_label = QLabel("Ready") self.status_label.setStyleSheet("color: #cccccc; padding: 5px;") layout.addWidget(self.status_label) self.setLayout(layout) # Initial load self.load_selected_log() def toggle_auto_refresh(self, enabled): """Toggle auto-refresh functionality.""" if enabled: # Adjust refresh interval based on file size self.adjust_refresh_interval() self.auto_refresh_timer.start(self.refresh_interval) self.status_label.setText(f"Auto-refresh enabled ({self.refresh_interval/1000:.1f}s interval)") else: self.auto_refresh_timer.stop() self.status_label.setText("Auto-refresh disabled") def adjust_refresh_interval(self): """Adjust refresh interval based on current log file size.""" selected_log = self.log_selector.currentText() if selected_log in self.log_files: log_path = Path(self.log_files[selected_log]) if log_path.exists(): file_size = log_path.stat().st_size # Increase interval for larger files if file_size > 500000: # > 500KB self.refresh_interval = 5000 # 5 seconds elif file_size > 100000: # > 100KB self.refresh_interval = 3000 # 3 seconds else: self.refresh_interval = 2000 # 2 seconds def update_max_lines(self, value): """Update maximum lines to display.""" self.max_lines = value self.load_selected_log() # Reload with new limit def load_selected_log(self): """Load the currently selected log file.""" selected_log = self.log_selector.currentText() if selected_log in self.log_files: self.load_log_file(self.log_files[selected_log]) def load_log_file(self, file_path): """Load content from a specific log file with optimization.""" try: log_path = Path(file_path) if not log_path.exists(): self.log_display.setPlainText(f"Log file not found: {file_path}") self.status_label.setText("Log file not found") return # Check if file has been modified since last read current_mod_time = log_path.stat().st_mtime last_mod_time = self.file_mod_times.get(file_path, 0) if current_mod_time <= last_mod_time and file_path in self.file_mod_times: # File hasn't changed, no need to reload return # Update modification time self.file_mod_times[file_path] = current_mod_time # Read file efficiently file_size = log_path.stat().st_size if file_size > 1000000: # > 1MB, read only tail content = self.read_file_tail(log_path, self.max_lines) else: # For smaller files, read normally but limit lines with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Limit to max lines if content is too long lines = content.splitlines() if len(lines) > self.max_lines: content = '\n'.join(lines[-self.max_lines:]) content = f"... (showing last {self.max_lines} lines) ...\n" + content self.log_display.setPlainText(content) # Scroll to bottom cursor = self.log_display.textCursor() cursor.movePosition(QTextCursor.End) self.log_display.setTextCursor(cursor) # Update status lines_count = len(content.splitlines()) self.status_label.setText(f"Loaded {log_path.name} ({file_size:,} bytes, {lines_count:,} lines)") except Exception as e: self.log_display.setPlainText(f"Error loading log file: {str(e)}") self.status_label.setText(f"Error: {str(e)}") def read_file_tail(self, file_path, max_lines): """Efficiently read the last N lines of a large file.""" try: with open(file_path, 'rb') as f: # Start from end of file f.seek(0, 2) # Seek to end file_size = f.tell() # Read chunks from end until we have enough lines lines = [] chunk_size = min(8192, file_size) # 8KB chunks pos = file_size while len(lines) < max_lines and pos > 0: # Calculate chunk position chunk_start = max(0, pos - chunk_size) f.seek(chunk_start) # Read chunk chunk = f.read(pos - chunk_start).decode('utf-8', errors='ignore') chunk_lines = chunk.splitlines() # Prepend to lines list if chunk_lines: if lines and not chunk.endswith('\n'): # Merge partial line at end of chunk with first existing line chunk_lines[-1] += lines[0] lines = chunk_lines + lines[1:] else: lines = chunk_lines + lines pos = chunk_start # Return last max_lines if len(lines) > max_lines: lines = lines[-max_lines:] return f"... (showing last {max_lines} lines) ...\n" + '\n'.join(lines) else: return '\n'.join(lines) except Exception as e: return f"Error reading file tail: {str(e)}" def refresh_logs(self): """Refresh the current log display - only if tab is visible and file changed.""" # Only refresh if widget is visible (optimization for when tab is not active) if not self.isVisible(): return self.load_selected_log() # Readjust refresh interval if auto-refresh is enabled if self.auto_refresh_cb.isChecked(): self.adjust_refresh_interval() if self.auto_refresh_timer.interval() != self.refresh_interval: self.auto_refresh_timer.stop() self.auto_refresh_timer.start(self.refresh_interval) def clear_log(self): """Clear the log display and reset tracking.""" self.log_display.clear() # Reset modification time tracking to force reload on next refresh selected_log = self.log_selector.currentText() if selected_log in self.log_files: file_path = self.log_files[selected_log] if file_path in self.file_mod_times: del self.file_mod_times[file_path] self.status_label.setText("Log cleared") def append_log_message(self, message): """Append a message to the log display.""" timestamp = datetime.datetime.now().strftime("%H:%M:%S") formatted_message = f"[{timestamp}] {message}" self.log_display.append(formatted_message) # Scroll to bottom cursor = self.log_display.textCursor() cursor.movePosition(QTextCursor.End) self.log_display.setTextCursor(cursor) def get_current_log_content(self): """Get the current log content.""" return self.log_display.toPlainText() def showEvent(self, event): """Handle widget show event - resume refresh if auto-refresh is enabled.""" super().showEvent(event) if self.auto_refresh_cb.isChecked() and not self.auto_refresh_timer.isActive(): self.auto_refresh_timer.start(self.refresh_interval) def hideEvent(self, event): """Handle widget hide event - pause refresh to save resources.""" super().hideEvent(event) # Note: Don't stop timer completely as other parts may depend on it # The refresh_logs method now checks visibility anyway