pyqt_reactive.widgets.log_viewer
PyQt6 Log Viewer Window
Provides comprehensive log viewing capabilities with real-time tailing, search functionality, and integration with OpenHCS subprocess execution. Reimplements log viewing using Qt widgets for native desktop integration.
Classes
Signals for background highlighting worker. |
|
|
Background worker that does expensive regex parsing to extract formatting info. |
|
A segment of text with formatting information. |
|
Centralized color scheme for log highlighting with semantic color names. |
|
Detects new log files in directory using efficient file monitoring. |
|
Background process for loading large log files without blocking UI. |
|
Advanced syntax highlighter for log files using Pygments. |
|
Delegate responsible for per-line coloring, regex-based syntax, and search highlighting. |
|
Lightweight list model storing log lines in a bounded ring buffer. |
|
Custom QListView that enables text selection within and across items. |
|
Background thread for tailing log files without blocking UI. |
|
Main log viewer window with dropdown, search, and real-time tailing. |
|
QTextDocument wrapper that manages text cursor and selection state. |
- class pyqt_reactive.widgets.log_viewer.LogColorScheme(log_critical_fg: Tuple[int, int, int] = (255, 255, 255), log_critical_bg: Tuple[int, int, int] = (139, 0, 0), log_error_color: Tuple[int, int, int] = (255, 85, 85), log_warning_color: Tuple[int, int, int] = (255, 140, 0), log_info_color: Tuple[int, int, int] = (100, 160, 210), log_debug_color: Tuple[int, int, int] = (160, 160, 160), timestamp_color: Tuple[int, int, int] = (105, 105, 105), logger_name_color: Tuple[int, int, int] = (147, 112, 219), memory_address_color: Tuple[int, int, int] = (255, 182, 193), file_path_color: Tuple[int, int, int] = (34, 139, 34), python_keyword_color: Tuple[int, int, int] = (86, 156, 214), python_string_color: Tuple[int, int, int] = (206, 145, 120), python_number_color: Tuple[int, int, int] = (181, 206, 168), python_operator_color: Tuple[int, int, int] = (212, 212, 212), python_name_color: Tuple[int, int, int] = (156, 220, 254), python_function_color: Tuple[int, int, int] = (220, 220, 170), python_class_color: Tuple[int, int, int] = (78, 201, 176), python_builtin_color: Tuple[int, int, int] = (86, 156, 214), python_comment_color: Tuple[int, int, int] = (106, 153, 85), exception_color: Tuple[int, int, int] = (255, 69, 0), function_call_color: Tuple[int, int, int] = (255, 215, 0), boolean_color: Tuple[int, int, int] = (86, 156, 214), tuple_parentheses_color: Tuple[int, int, int] = (255, 215, 0), set_braces_color: Tuple[int, int, int] = (255, 140, 0), class_representation_color: Tuple[int, int, int] = (78, 201, 176), function_representation_color: Tuple[int, int, int] = (220, 220, 170), module_path_color: Tuple[int, int, int] = (147, 112, 219), hex_number_color: Tuple[int, int, int] = (181, 206, 168), scientific_notation_color: Tuple[int, int, int] = (181, 206, 168), binary_number_color: Tuple[int, int, int] = (181, 206, 168), octal_number_color: Tuple[int, int, int] = (181, 206, 168), python_special_color: Tuple[int, int, int] = (255, 20, 147), single_quoted_string_color: Tuple[int, int, int] = (206, 145, 120), list_comprehension_color: Tuple[int, int, int] = (156, 220, 254), generator_expression_color: Tuple[int, int, int] = (156, 220, 254))[source]
Centralized color scheme for log highlighting with semantic color names.
Supports light/dark theme variants and ensures WCAG accessibility compliance. All colors meet minimum 4.5:1 contrast ratio for normal text readability.
- classmethod create_dark_theme() LogColorScheme[source]
Create a dark theme variant with adjusted colors for dark backgrounds.
- Returns:
Dark theme color scheme with higher contrast
- Return type:
- classmethod create_light_theme() LogColorScheme[source]
Create a light theme variant with adjusted colors for light backgrounds.
- Returns:
Light theme color scheme with appropriate contrast
- Return type:
- to_qcolor(color_tuple: Tuple[int, int, int]) QColor[source]
Convert RGB tuple to QColor object.
- Parameters:
color_tuple – RGB color tuple (r, g, b)
- Returns:
Corresponding QColor instance.
- Return type:
QColor
- __init__(log_critical_fg: Tuple[int, int, int] = (255, 255, 255), log_critical_bg: Tuple[int, int, int] = (139, 0, 0), log_error_color: Tuple[int, int, int] = (255, 85, 85), log_warning_color: Tuple[int, int, int] = (255, 140, 0), log_info_color: Tuple[int, int, int] = (100, 160, 210), log_debug_color: Tuple[int, int, int] = (160, 160, 160), timestamp_color: Tuple[int, int, int] = (105, 105, 105), logger_name_color: Tuple[int, int, int] = (147, 112, 219), memory_address_color: Tuple[int, int, int] = (255, 182, 193), file_path_color: Tuple[int, int, int] = (34, 139, 34), python_keyword_color: Tuple[int, int, int] = (86, 156, 214), python_string_color: Tuple[int, int, int] = (206, 145, 120), python_number_color: Tuple[int, int, int] = (181, 206, 168), python_operator_color: Tuple[int, int, int] = (212, 212, 212), python_name_color: Tuple[int, int, int] = (156, 220, 254), python_function_color: Tuple[int, int, int] = (220, 220, 170), python_class_color: Tuple[int, int, int] = (78, 201, 176), python_builtin_color: Tuple[int, int, int] = (86, 156, 214), python_comment_color: Tuple[int, int, int] = (106, 153, 85), exception_color: Tuple[int, int, int] = (255, 69, 0), function_call_color: Tuple[int, int, int] = (255, 215, 0), boolean_color: Tuple[int, int, int] = (86, 156, 214), tuple_parentheses_color: Tuple[int, int, int] = (255, 215, 0), set_braces_color: Tuple[int, int, int] = (255, 140, 0), class_representation_color: Tuple[int, int, int] = (78, 201, 176), function_representation_color: Tuple[int, int, int] = (220, 220, 170), module_path_color: Tuple[int, int, int] = (147, 112, 219), hex_number_color: Tuple[int, int, int] = (181, 206, 168), scientific_notation_color: Tuple[int, int, int] = (181, 206, 168), binary_number_color: Tuple[int, int, int] = (181, 206, 168), octal_number_color: Tuple[int, int, int] = (181, 206, 168), python_special_color: Tuple[int, int, int] = (255, 20, 147), single_quoted_string_color: Tuple[int, int, int] = (206, 145, 120), list_comprehension_color: Tuple[int, int, int] = (156, 220, 254), generator_expression_color: Tuple[int, int, int] = (156, 220, 254)) None
- class pyqt_reactive.widgets.log_viewer.LogListModel(parent: QObject | None = None)[source]
Lightweight list model storing log lines in a bounded ring buffer.
The model is pure data; rendering and highlighting are handled by a delegate on top of this model.
- MAX_LINES = 100000
- class pyqt_reactive.widgets.log_viewer.HighlightedSegment(start: int, length: int, color: Tuple[int, int, int], bold: bool = False, italic: bool = False)[source]
A segment of text with formatting information.
- class pyqt_reactive.widgets.log_viewer.SelectableDocument(parent: QObject | None = None)[source]
QTextDocument wrapper that manages text cursor and selection state.
Enables text selection within QListView delegate items by tracking cursor position and applying visual selection highlighting using QTextCharFormat.
Based on Qt Forum solution for interactive elements in delegates.
- setTextWidth(width: float) None[source]
Override to avoid unnecessary relayout when width hasn’t changed.
- setCursorPosition(pos: int, mode: MoveMode = MoveMode.MoveAnchor) None[source]
Set cursor position and update selection highlighting.
- Parameters:
pos – Character position in document
mode – MoveAnchor (clear selection) or KeepAnchor (extend selection)
- moveCursorPosition(op: MoveOperation, mode: MoveMode = MoveMode.MoveAnchor, n: int = 1) bool[source]
Move cursor by operation and update highlighting.
- Parameters:
op – Move operation (e.g., StartOfWord, EndOfWord)
mode – MoveAnchor or KeepAnchor
n – Number of times to repeat operation
- Returns:
True if move was successful
- select(selection: SelectionType) None[source]
Select text by type (word, line, etc.) and update highlighting.
- Parameters:
selection – Type of selection (WordUnderCursor, LineUnderCursor, Document)
- extendWordwiseSelection(newPos: int) None[source]
Extend selection to word boundaries when dragging after double-click.
This implements the behavior where double-clicking selects a word, then dragging extends the selection by whole words rather than characters.
- Parameters:
newPos – New cursor position from mouse drag
- class pyqt_reactive.widgets.log_viewer.HighlightSignals[source]
Signals for background highlighting worker.
- finished
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- class pyqt_reactive.widgets.log_viewer.HighlightWorker(text: str, cache_key: Tuple[str, str, int], color_scheme: LogColorScheme, signals: HighlightSignals)[source]
Background worker that does expensive regex parsing to extract formatting info.
This runs all the expensive regex matching in a background thread and returns just the formatting information (which segments should be what color). The main thread then applies this to a QTextDocument.
- __init__(text: str, cache_key: Tuple[str, str, int], color_scheme: LogColorScheme, signals: HighlightSignals)[source]
- class pyqt_reactive.widgets.log_viewer.LogItemDelegate(color_scheme: LogColorScheme | None = None, parent: QWidget | None = None)[source]
Delegate responsible for per-line coloring, regex-based syntax, and search highlighting.
We avoid document-wide highlighting and instead reuse the existing LogHighlighter rules on a per-line QTextDocument that is only used for visible rows. This keeps work bounded by visible rows and line length, independent of total log size.
Performance optimization: 1. Cache highlighted QTextDocuments to avoid re-running expensive regex highlighting 2. Perform highlighting in background thread pool to never block UI 3. Paint with plain text if highlighting not ready yet (progressive enhancement)
- MAX_CACHE_SIZE = 10000
- set_search_state(text: str, case_sensitive: bool) None[source]
Update search text used for line-level search highlighting.
- hasInteractiveDocument(row: int) bool[source]
Check if a row has an interactive document with selection.
- getInteractiveDocument(row: int) SelectableDocument | None[source]
Get the interactive document for a specific row.
- setInteractiveDocument(row: int, doc: SelectableDocument) None[source]
Set the interactive document for a specific row.
- createInteractiveDocument(text: str, font: QFont, width: int = 0) SelectableDocument[source]
Create a SelectableDocument with syntax highlighting applied.
This is used by LogListView when creating a new interactive document for text selection. It applies the same syntax highlighting as normal rendering.
- Parameters:
text – The log line text
font – The font to use for rendering
width – The width for text wrapping (optional)
- Returns:
SelectableDocument with syntax highlighting applied
- sizeHint(option, index)[source]
Return height large enough to show wrapped text for this row.
We compute the document layout for the current line using the available viewport width so that long log lines wrap instead of being clipped or forcing a horizontal scrollbar.
This method is heavily optimized with caching since Qt calls it frequently during scrolling, resizing, and layout updates.
- class pyqt_reactive.widgets.log_viewer.LogListView(parent: QWidget | None = None)[source]
Custom QListView that enables text selection within and across items.
Handles mouse events to track cursor position in QTextDocument and manage text selection state. Supports single-line and multi-line selection. Works in conjunction with LogItemDelegate to paint selection highlighting.
Based on Qt Forum solution for interactive elements in delegates.
- class pyqt_reactive.widgets.log_viewer.LogFileDetector(base_log_path: str | None = None)[source]
Detects new log files in directory using efficient file monitoring.
Uses QFileSystemWatcher to monitor directory changes and set operations for efficient new file detection. Handles base_log_path as file prefix and watches the parent directory.
- new_log_detected
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- __init__(base_log_path: str | None = None)[source]
Initialize LogFileDetector.
- Parameters:
base_log_path – Base path for subprocess log files (file prefix, not directory)
- start_watching(directory: Path) None[source]
Start watching directory for new log files.
- Parameters:
directory – Directory to watch for new log files
- class pyqt_reactive.widgets.log_viewer.LogHighlighter(parent: QTextDocument, color_scheme: LogColorScheme = None)[source]
Advanced syntax highlighter for log files using Pygments.
Provides sophisticated highlighting for OpenHCS log format with support for: - Log levels and timestamps - Python code snippets and data structures - Memory addresses and function signatures - Complex nested dictionaries and lists - Exception tracebacks and file paths
- __init__(parent: QTextDocument, color_scheme: LogColorScheme = None)[source]
Initialize the log highlighter with optional color scheme.
- Parameters:
parent – QTextDocument to apply highlighting to
color_scheme – Color scheme to use (defaults to dark theme)
- setup_pygments_styles() None[source]
Setup Pygments token to QTextCharFormat mapping using color scheme.
- set_color_scheme(color_scheme: LogColorScheme) None[source]
Update the color scheme and refresh highlighting.
- Parameters:
color_scheme – New color scheme to apply
- classmethod load_color_scheme_from_config(config_path: str = None) LogColorScheme[source]
Load color scheme from external configuration file.
- Parameters:
config_path – Path to JSON/YAML config file (optional)
- Returns:
Loaded color scheme or default if file not found
- Return type:
- class pyqt_reactive.widgets.log_viewer.LogFileLoader(log_path: Path, tail_lines: int = 100000, chunk_lines: int = 1000)[source]
Background process for loading large log files without blocking UI.
- chunk_loaded
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- load_finished
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- load_failed
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- class pyqt_reactive.widgets.log_viewer.LogTailer(log_path: Path, initial_position: int = 0)[source]
Background thread for tailing log files without blocking UI.
- new_content
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- log_rotated
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- error_occurred
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- class pyqt_reactive.widgets.log_viewer.LogViewerWindow(file_manager, service_adapter, parent=None)[source]
Main log viewer window with dropdown, search, and real-time tailing.
- window_closed
int = …, arguments: Sequence = …) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal’s arguments.
- Type:
pyqtSignal(*types, name
- Type:
str = …, revision
- log_selector: QComboBox
- search_toolbar: QToolBar
- log_view: QListView
- log_model: LogListModel | None
- log_delegate: LogItemDelegate | None
- file_detector: LogFileDetector
- tail_timer: QTimer
- highlighter: LogHighlighter | None
- file_loader: LogFileLoader | None
- server_scan_timer: QTimer
- process_update_timer: QTimer
- initialize_logs() None[source]
Initialize with main log only, then scan for subprocess logs in background.
- populate_log_dropdown(log_files: List[LogFileInfo]) None[source]
Populate QComboBox with log files with process status indicators.
- Parameters:
log_files – List of LogFileInfo objects to add to dropdown
- add_new_log(log_file_info: LogFileInfo) None[source]
Add new log to dropdown maintaining sort order.
- Parameters:
log_file_info – New LogFileInfo to add
- on_log_selection_changed(index: int) None[source]
Handle dropdown selection change - switch log display.
- Parameters:
index – Selected index in dropdown
- switch_to_log(log_path: Path) None[source]
Switch log display to show specified log file.
- Parameters:
log_path – Path to log file to display
- reset_memory() None[source]
Reset memory usage by clearing the in-memory log model and reloading current log.
This completely clears the text buffer and reloads the log file from disk, freeing memory accumulated from long-running log tailing.
- start_log_tailing(log_path: Path) None[source]
Start tailing log file with async background thread (never blocks UI).
- Parameters:
log_path – Path to log file to tail
- read_log_incremental() None[source]
Deprecated - kept for compatibility. Tailing now handled by LogTailer thread.