287 lines
12 KiB
Python
287 lines
12 KiB
Python
"""
|
|
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
|