pomera-ai-commander 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +680 -0
- package/bin/pomera-ai-commander.js +62 -0
- package/core/__init__.py +66 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/app_context.py +482 -0
- package/core/async_text_processor.py +422 -0
- package/core/backup_manager.py +656 -0
- package/core/backup_recovery_manager.py +1034 -0
- package/core/content_hash_cache.py +509 -0
- package/core/context_menu.py +313 -0
- package/core/data_validator.py +1067 -0
- package/core/database_connection_manager.py +745 -0
- package/core/database_curl_settings_manager.py +609 -0
- package/core/database_promera_ai_settings_manager.py +447 -0
- package/core/database_schema.py +412 -0
- package/core/database_schema_manager.py +396 -0
- package/core/database_settings_manager.py +1508 -0
- package/core/database_settings_manager_interface.py +457 -0
- package/core/dialog_manager.py +735 -0
- package/core/efficient_line_numbers.py +511 -0
- package/core/error_handler.py +747 -0
- package/core/error_service.py +431 -0
- package/core/event_consolidator.py +512 -0
- package/core/mcp/__init__.py +43 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/core/mcp/protocol.py +288 -0
- package/core/mcp/schema.py +251 -0
- package/core/mcp/server_stdio.py +299 -0
- package/core/mcp/tool_registry.py +2345 -0
- package/core/memory_efficient_text_widget.py +712 -0
- package/core/migration_manager.py +915 -0
- package/core/migration_test_suite.py +1086 -0
- package/core/migration_validator.py +1144 -0
- package/core/optimized_find_replace.py +715 -0
- package/core/optimized_pattern_engine.py +424 -0
- package/core/optimized_search_highlighter.py +553 -0
- package/core/performance_monitor.py +675 -0
- package/core/persistence_manager.py +713 -0
- package/core/progressive_stats_calculator.py +632 -0
- package/core/regex_pattern_cache.py +530 -0
- package/core/regex_pattern_library.py +351 -0
- package/core/search_operation_manager.py +435 -0
- package/core/settings_defaults_registry.py +1087 -0
- package/core/settings_integrity_validator.py +1112 -0
- package/core/settings_serializer.py +558 -0
- package/core/settings_validator.py +1824 -0
- package/core/smart_stats_calculator.py +710 -0
- package/core/statistics_update_manager.py +619 -0
- package/core/stats_config_manager.py +858 -0
- package/core/streaming_text_handler.py +723 -0
- package/core/task_scheduler.py +596 -0
- package/core/update_pattern_library.py +169 -0
- package/core/visibility_monitor.py +596 -0
- package/core/widget_cache.py +498 -0
- package/mcp.json +61 -0
- package/package.json +57 -0
- package/pomera.py +7483 -0
- package/pomera_mcp_server.py +144 -0
- package/tools/__init__.py +5 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
- package/tools/ai_tools.py +2892 -0
- package/tools/ascii_art_generator.py +353 -0
- package/tools/base64_tools.py +184 -0
- package/tools/base_tool.py +511 -0
- package/tools/case_tool.py +309 -0
- package/tools/column_tools.py +396 -0
- package/tools/cron_tool.py +885 -0
- package/tools/curl_history.py +601 -0
- package/tools/curl_processor.py +1208 -0
- package/tools/curl_settings.py +503 -0
- package/tools/curl_tool.py +5467 -0
- package/tools/diff_viewer.py +1072 -0
- package/tools/email_extraction_tool.py +249 -0
- package/tools/email_header_analyzer.py +426 -0
- package/tools/extraction_tools.py +250 -0
- package/tools/find_replace.py +1751 -0
- package/tools/folder_file_reporter.py +1463 -0
- package/tools/folder_file_reporter_adapter.py +480 -0
- package/tools/generator_tools.py +1217 -0
- package/tools/hash_generator.py +256 -0
- package/tools/html_tool.py +657 -0
- package/tools/huggingface_helper.py +449 -0
- package/tools/jsonxml_tool.py +730 -0
- package/tools/line_tools.py +419 -0
- package/tools/list_comparator.py +720 -0
- package/tools/markdown_tools.py +562 -0
- package/tools/mcp_widget.py +1417 -0
- package/tools/notes_widget.py +973 -0
- package/tools/number_base_converter.py +373 -0
- package/tools/regex_extractor.py +572 -0
- package/tools/slug_generator.py +311 -0
- package/tools/sorter_tools.py +459 -0
- package/tools/string_escape_tool.py +393 -0
- package/tools/text_statistics_tool.py +366 -0
- package/tools/text_wrapper.py +431 -0
- package/tools/timestamp_converter.py +422 -0
- package/tools/tool_loader.py +710 -0
- package/tools/translator_tools.py +523 -0
- package/tools/url_link_extractor.py +262 -0
- package/tools/url_parser.py +205 -0
- package/tools/whitespace_tools.py +356 -0
- package/tools/word_frequency_counter.py +147 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diff Viewer Tool Module
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive text comparison tool with multiple diff algorithms
|
|
5
|
+
and preprocessing options. It supports side-by-side comparison with synchronized scrolling
|
|
6
|
+
and word-level highlighting of differences.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Multiple comparison modes (ignore case, match case, ignore whitespace)
|
|
10
|
+
- Side-by-side text comparison with synchronized scrolling
|
|
11
|
+
- Word-level difference highlighting
|
|
12
|
+
- Tab-based interface for multiple comparisons
|
|
13
|
+
- Integration with optimized text widgets when available
|
|
14
|
+
|
|
15
|
+
Author: Promera AI Commander
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import tkinter as tk
|
|
19
|
+
from tkinter import ttk
|
|
20
|
+
import re
|
|
21
|
+
import platform
|
|
22
|
+
import logging
|
|
23
|
+
import subprocess
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from typing import Dict, Any, List, Optional
|
|
27
|
+
|
|
28
|
+
# Import optimized components when available
|
|
29
|
+
try:
|
|
30
|
+
from core.efficient_line_numbers import OptimizedTextWithLineNumbers
|
|
31
|
+
EFFICIENT_LINE_NUMBERS_AVAILABLE = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
EFFICIENT_LINE_NUMBERS_AVAILABLE = False
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from core.memory_efficient_text_widget import MemoryEfficientTextWidget
|
|
37
|
+
MEMORY_EFFICIENT_TEXT_AVAILABLE = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
MEMORY_EFFICIENT_TEXT_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TextWithLineNumbers(tk.Frame):
|
|
43
|
+
"""Fallback implementation of TextWithLineNumbers when optimized components are not available."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, *args, **kwargs):
|
|
46
|
+
super().__init__(*args, **kwargs)
|
|
47
|
+
self.text = tk.Text(self, wrap=tk.WORD, height=15, width=50, undo=True)
|
|
48
|
+
self.linenumbers = tk.Canvas(self, width=40, bg='#f0f0f0', highlightthickness=0)
|
|
49
|
+
|
|
50
|
+
self.linenumbers.pack(side=tk.LEFT, fill=tk.Y)
|
|
51
|
+
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
52
|
+
|
|
53
|
+
# Basic event bindings
|
|
54
|
+
self.text.bind("<<Modified>>", self._on_text_modified)
|
|
55
|
+
self.text.bind("<Configure>", self._on_text_modified)
|
|
56
|
+
self._on_text_modified()
|
|
57
|
+
|
|
58
|
+
def _on_text_modified(self, event=None):
|
|
59
|
+
"""Update line numbers when text is modified."""
|
|
60
|
+
self.linenumbers.delete("all")
|
|
61
|
+
line_info_cache = []
|
|
62
|
+
i = self.text.index("@0,0")
|
|
63
|
+
while True:
|
|
64
|
+
dline = self.text.dlineinfo(i)
|
|
65
|
+
if dline is None:
|
|
66
|
+
break
|
|
67
|
+
line_info_cache.append((i, dline[1]))
|
|
68
|
+
i = self.text.index("%s+1line" % i)
|
|
69
|
+
|
|
70
|
+
for i, y in line_info_cache:
|
|
71
|
+
linenum = str(i).split(".")[0]
|
|
72
|
+
self.linenumbers.create_text(20, y, anchor="n", text=linenum, fill="gray")
|
|
73
|
+
|
|
74
|
+
if event and hasattr(event.widget, 'edit_modified') and event.widget.edit_modified():
|
|
75
|
+
event.widget.edit_modified(False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DiffViewerWidget:
|
|
79
|
+
"""
|
|
80
|
+
A comprehensive diff viewer widget that provides side-by-side text comparison
|
|
81
|
+
with multiple comparison algorithms and preprocessing options.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, parent, tab_count=7, logger=None, parent_callback=None, dialog_manager=None):
|
|
85
|
+
"""
|
|
86
|
+
Initialize the diff viewer widget.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
parent: Parent tkinter widget
|
|
90
|
+
tab_count: Number of tabs to create (default: 7)
|
|
91
|
+
logger: Logger instance for debugging
|
|
92
|
+
parent_callback: Callback function to notify parent of changes
|
|
93
|
+
dialog_manager: DialogManager instance for consistent dialog handling
|
|
94
|
+
"""
|
|
95
|
+
self.parent = parent
|
|
96
|
+
self.tab_count = tab_count
|
|
97
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
98
|
+
self.parent_callback = parent_callback
|
|
99
|
+
self.dialog_manager = dialog_manager
|
|
100
|
+
|
|
101
|
+
# Settings for diff comparison
|
|
102
|
+
self.settings = {
|
|
103
|
+
"option": "ignore_case" # Default comparison mode
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Create the main frame
|
|
107
|
+
self.diff_frame = ttk.Frame(parent, padding="10")
|
|
108
|
+
self.diff_frame.grid_columnconfigure(0, weight=1)
|
|
109
|
+
self.diff_frame.grid_columnconfigure(1, weight=1)
|
|
110
|
+
self.diff_frame.grid_rowconfigure(1, weight=1)
|
|
111
|
+
|
|
112
|
+
# Statistics bars
|
|
113
|
+
self.input_stats_bar = None
|
|
114
|
+
self.output_stats_bar = None
|
|
115
|
+
|
|
116
|
+
# Initialize UI components
|
|
117
|
+
self._create_ui()
|
|
118
|
+
self._setup_event_bindings()
|
|
119
|
+
|
|
120
|
+
def _show_error(self, title, message):
|
|
121
|
+
"""Show error dialog using DialogManager if available, otherwise use messagebox."""
|
|
122
|
+
if self.dialog_manager:
|
|
123
|
+
return self.dialog_manager.show_error(title, message, parent=self.parent)
|
|
124
|
+
else:
|
|
125
|
+
try:
|
|
126
|
+
from tkinter import messagebox
|
|
127
|
+
messagebox.showerror(title, message, parent=self.parent)
|
|
128
|
+
return True
|
|
129
|
+
except:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _show_warning(self, title, message, category="warning"):
|
|
133
|
+
"""Show warning dialog using DialogManager if available, otherwise use messagebox."""
|
|
134
|
+
if self.dialog_manager:
|
|
135
|
+
return self.dialog_manager.show_warning(title, message, category, parent=self.parent)
|
|
136
|
+
else:
|
|
137
|
+
try:
|
|
138
|
+
import tkinter.messagebox as messagebox
|
|
139
|
+
messagebox.showwarning(title, message, parent=self.parent)
|
|
140
|
+
return True
|
|
141
|
+
except:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def _create_ui(self):
|
|
145
|
+
"""Create the user interface components."""
|
|
146
|
+
self._create_title_rows()
|
|
147
|
+
self._create_notebooks()
|
|
148
|
+
self._create_tabs()
|
|
149
|
+
self._create_statistics_bars()
|
|
150
|
+
self._configure_text_tags()
|
|
151
|
+
|
|
152
|
+
def _create_title_rows(self):
|
|
153
|
+
"""Create the title rows with buttons and controls."""
|
|
154
|
+
# Input title row
|
|
155
|
+
input_title_row = ttk.Frame(self.diff_frame)
|
|
156
|
+
input_title_row.grid(row=0, column=0, sticky="ew", padx=(0, 5))
|
|
157
|
+
|
|
158
|
+
# Input label and buttons
|
|
159
|
+
input_controls = ttk.Frame(input_title_row)
|
|
160
|
+
input_controls.pack(side=tk.TOP, fill=tk.X)
|
|
161
|
+
ttk.Label(input_controls, text="Input", font=("Helvetica", 12, "bold")).pack(side=tk.LEFT)
|
|
162
|
+
|
|
163
|
+
# Load from file button
|
|
164
|
+
load_file_btn = ttk.Button(input_controls, text="📁", command=self.load_file_to_input, width=3)
|
|
165
|
+
load_file_btn.pack(side=tk.LEFT, padx=(10, 0))
|
|
166
|
+
|
|
167
|
+
# Erase button
|
|
168
|
+
ttk.Button(input_controls, text="⌫", command=self.clear_all_input_tabs, width=3).pack(side=tk.LEFT, padx=(5, 0))
|
|
169
|
+
|
|
170
|
+
# Input line filter
|
|
171
|
+
input_filter_frame = ttk.Frame(input_title_row)
|
|
172
|
+
input_filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
|
|
173
|
+
ttk.Label(input_filter_frame, text="Filter:").pack(side=tk.LEFT)
|
|
174
|
+
self.input_filter_var = tk.StringVar()
|
|
175
|
+
self.input_filter_entry = ttk.Entry(input_filter_frame, textvariable=self.input_filter_var, width=25)
|
|
176
|
+
self.input_filter_entry.pack(side=tk.LEFT, padx=(5, 5), fill=tk.X, expand=True)
|
|
177
|
+
self.input_filter_var.trace_add("write", self._on_input_filter_changed)
|
|
178
|
+
ttk.Button(input_filter_frame, text="✕", command=self._clear_input_filter, width=3).pack(side=tk.LEFT)
|
|
179
|
+
|
|
180
|
+
# Output title row
|
|
181
|
+
output_title_row = ttk.Frame(self.diff_frame)
|
|
182
|
+
output_title_row.grid(row=0, column=1, sticky="ew", padx=(5, 0))
|
|
183
|
+
|
|
184
|
+
# Output label and buttons
|
|
185
|
+
output_controls = ttk.Frame(output_title_row)
|
|
186
|
+
output_controls.pack(side=tk.TOP, fill=tk.X)
|
|
187
|
+
ttk.Label(output_controls, text="Output", font=("Helvetica", 12, "bold")).pack(side=tk.LEFT)
|
|
188
|
+
|
|
189
|
+
# Send to Input dropdown
|
|
190
|
+
self.send_to_input_var = tk.StringVar(value="Send to Input")
|
|
191
|
+
send_to_input_menu = ttk.Menubutton(output_controls, textvariable=self.send_to_input_var, direction="below")
|
|
192
|
+
send_to_input_menu.pack(side=tk.LEFT, padx=(10, 6))
|
|
193
|
+
|
|
194
|
+
# Create the dropdown menu
|
|
195
|
+
dropdown_menu = tk.Menu(send_to_input_menu, tearoff=0)
|
|
196
|
+
send_to_input_menu.config(menu=dropdown_menu)
|
|
197
|
+
for i in range(self.tab_count):
|
|
198
|
+
dropdown_menu.add_command(label=f"Tab {i+1}", command=lambda tab=i: self.copy_to_specific_input_tab(tab))
|
|
199
|
+
|
|
200
|
+
# Copy to clipboard button
|
|
201
|
+
ttk.Button(output_controls, text="⎘", command=self.copy_to_clipboard, width=3).pack(side=tk.LEFT, padx=(0, 6))
|
|
202
|
+
|
|
203
|
+
# Erase button
|
|
204
|
+
ttk.Button(output_controls, text="⌫", command=self.clear_all_output_tabs, width=3).pack(side=tk.LEFT)
|
|
205
|
+
|
|
206
|
+
# Output line filter
|
|
207
|
+
output_filter_frame = ttk.Frame(output_title_row)
|
|
208
|
+
output_filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0))
|
|
209
|
+
ttk.Label(output_filter_frame, text="Filter:").pack(side=tk.LEFT)
|
|
210
|
+
self.output_filter_var = tk.StringVar()
|
|
211
|
+
self.output_filter_entry = ttk.Entry(output_filter_frame, textvariable=self.output_filter_var, width=25)
|
|
212
|
+
self.output_filter_entry.pack(side=tk.LEFT, padx=(5, 5), fill=tk.X, expand=True)
|
|
213
|
+
self.output_filter_var.trace_add("write", self._on_output_filter_changed)
|
|
214
|
+
ttk.Button(output_filter_frame, text="✕", command=self._clear_output_filter, width=3).pack(side=tk.LEFT)
|
|
215
|
+
|
|
216
|
+
# Store original content for filtering
|
|
217
|
+
self.input_original_content = {}
|
|
218
|
+
self.output_original_content = {}
|
|
219
|
+
|
|
220
|
+
def _create_notebooks(self):
|
|
221
|
+
"""Create the notebook widgets for input and output tabs."""
|
|
222
|
+
self.input_notebook = ttk.Notebook(self.diff_frame)
|
|
223
|
+
self.input_notebook.grid(row=1, column=0, sticky="nsew", padx=(0, 5))
|
|
224
|
+
|
|
225
|
+
self.output_notebook = ttk.Notebook(self.diff_frame)
|
|
226
|
+
self.output_notebook.grid(row=1, column=1, sticky="nsew", padx=(5, 0))
|
|
227
|
+
|
|
228
|
+
def _create_statistics_bars(self):
|
|
229
|
+
"""Create statistics bars below the text areas."""
|
|
230
|
+
# Input statistics bar
|
|
231
|
+
self.input_stats_bar = ttk.Label(
|
|
232
|
+
self.diff_frame,
|
|
233
|
+
text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0",
|
|
234
|
+
relief=tk.SUNKEN,
|
|
235
|
+
anchor=tk.W,
|
|
236
|
+
padding=(5, 2)
|
|
237
|
+
)
|
|
238
|
+
self.input_stats_bar.grid(row=2, column=0, sticky="ew", padx=(0, 5), pady=(5, 0))
|
|
239
|
+
|
|
240
|
+
# Output statistics bar
|
|
241
|
+
self.output_stats_bar = ttk.Label(
|
|
242
|
+
self.diff_frame,
|
|
243
|
+
text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0",
|
|
244
|
+
relief=tk.SUNKEN,
|
|
245
|
+
anchor=tk.W,
|
|
246
|
+
padding=(5, 2)
|
|
247
|
+
)
|
|
248
|
+
self.output_stats_bar.grid(row=2, column=1, sticky="ew", padx=(5, 0), pady=(5, 0))
|
|
249
|
+
|
|
250
|
+
def _create_tabs(self):
|
|
251
|
+
"""Create the text tabs for input and output."""
|
|
252
|
+
self.input_tabs = []
|
|
253
|
+
self.output_tabs = []
|
|
254
|
+
|
|
255
|
+
for i in range(self.tab_count):
|
|
256
|
+
# Create input tab
|
|
257
|
+
if EFFICIENT_LINE_NUMBERS_AVAILABLE:
|
|
258
|
+
input_tab = OptimizedTextWithLineNumbers(self.input_notebook)
|
|
259
|
+
elif MEMORY_EFFICIENT_TEXT_AVAILABLE:
|
|
260
|
+
input_tab = MemoryEfficientTextWidget(self.input_notebook)
|
|
261
|
+
else:
|
|
262
|
+
input_tab = TextWithLineNumbers(self.input_notebook)
|
|
263
|
+
|
|
264
|
+
input_tab.text.bind("<<Modified>>", self._on_tab_content_changed)
|
|
265
|
+
input_tab.text.bind("<KeyRelease>", self._on_tab_content_changed)
|
|
266
|
+
input_tab.text.bind("<Button-1>", self._on_tab_content_changed)
|
|
267
|
+
self.input_tabs.append(input_tab)
|
|
268
|
+
self.input_notebook.add(input_tab, text=f"{i+1}:")
|
|
269
|
+
|
|
270
|
+
# Create output tab
|
|
271
|
+
if EFFICIENT_LINE_NUMBERS_AVAILABLE:
|
|
272
|
+
output_tab = OptimizedTextWithLineNumbers(self.output_notebook)
|
|
273
|
+
elif MEMORY_EFFICIENT_TEXT_AVAILABLE:
|
|
274
|
+
output_tab = MemoryEfficientTextWidget(self.output_notebook)
|
|
275
|
+
else:
|
|
276
|
+
output_tab = TextWithLineNumbers(self.output_notebook)
|
|
277
|
+
|
|
278
|
+
output_tab.text.bind("<<Modified>>", self._on_tab_content_changed)
|
|
279
|
+
output_tab.text.bind("<KeyRelease>", self._on_tab_content_changed)
|
|
280
|
+
output_tab.text.bind("<Button-1>", self._on_tab_content_changed)
|
|
281
|
+
self.output_tabs.append(output_tab)
|
|
282
|
+
self.output_notebook.add(output_tab, text=f"{i+1}:")
|
|
283
|
+
|
|
284
|
+
def _configure_text_tags(self):
|
|
285
|
+
"""Configure text tags for highlighting differences."""
|
|
286
|
+
for tab_list in [self.input_tabs, self.output_tabs]:
|
|
287
|
+
for tab in tab_list:
|
|
288
|
+
widget = tab.text
|
|
289
|
+
widget.config(state="normal")
|
|
290
|
+
widget.tag_configure("addition", background="#e6ffed")
|
|
291
|
+
widget.tag_configure("deletion", background="#ffebe9")
|
|
292
|
+
widget.tag_configure("modification", background="#e6f7ff")
|
|
293
|
+
widget.tag_configure("inline_add", background="#a7f0ba")
|
|
294
|
+
widget.tag_configure("inline_del", background="#ffc9c9")
|
|
295
|
+
|
|
296
|
+
def _setup_event_bindings(self):
|
|
297
|
+
"""Set up event bindings for synchronized scrolling."""
|
|
298
|
+
self.input_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
|
299
|
+
self.output_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
|
300
|
+
self._setup_sync()
|
|
301
|
+
|
|
302
|
+
def _on_tab_changed(self, event=None):
|
|
303
|
+
"""Handle tab change events."""
|
|
304
|
+
# Clear filters when switching tabs
|
|
305
|
+
if hasattr(self, 'input_filter_var'):
|
|
306
|
+
self.input_filter_var.set("")
|
|
307
|
+
if hasattr(self, 'output_filter_var'):
|
|
308
|
+
self.output_filter_var.set("")
|
|
309
|
+
|
|
310
|
+
self._setup_sync(event)
|
|
311
|
+
self.update_statistics()
|
|
312
|
+
|
|
313
|
+
def _setup_sync(self, event=None):
|
|
314
|
+
"""Configure scroll and mousewheel syncing for the active tabs."""
|
|
315
|
+
try:
|
|
316
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
317
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
318
|
+
except (tk.TclError, IndexError):
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# Configure scrollbar syncing
|
|
322
|
+
if hasattr(active_input_tab.text, 'vbar'):
|
|
323
|
+
active_input_tab.text.vbar.config(command=self._sync_scroll)
|
|
324
|
+
if hasattr(active_output_tab.text, 'vbar'):
|
|
325
|
+
active_output_tab.text.vbar.config(command=self._sync_scroll)
|
|
326
|
+
|
|
327
|
+
# Configure mouse wheel syncing
|
|
328
|
+
for tab in [active_input_tab, active_output_tab]:
|
|
329
|
+
tab.text.bind("<MouseWheel>", self._on_mousewheel)
|
|
330
|
+
tab.text.bind("<Button-4>", self._on_mousewheel)
|
|
331
|
+
tab.text.bind("<Button-5>", self._on_mousewheel)
|
|
332
|
+
|
|
333
|
+
def _sync_scroll(self, *args):
|
|
334
|
+
"""Sync both text widgets when one's scrollbar is used."""
|
|
335
|
+
try:
|
|
336
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
337
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
338
|
+
|
|
339
|
+
active_input_tab.text.yview(*args)
|
|
340
|
+
active_output_tab.text.yview(*args)
|
|
341
|
+
|
|
342
|
+
# Update line numbers if available
|
|
343
|
+
if hasattr(active_input_tab, '_on_text_modified'):
|
|
344
|
+
active_input_tab._on_text_modified()
|
|
345
|
+
if hasattr(active_output_tab, '_on_text_modified'):
|
|
346
|
+
active_output_tab._on_text_modified()
|
|
347
|
+
except (tk.TclError, IndexError):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
def _on_mousewheel(self, event):
|
|
351
|
+
"""Handle mouse wheel scrolling over either text widget."""
|
|
352
|
+
if platform.system() == "Windows":
|
|
353
|
+
delta = int(-1*(event.delta/120))
|
|
354
|
+
elif platform.system() == "Darwin":
|
|
355
|
+
delta = int(-1 * event.delta)
|
|
356
|
+
else:
|
|
357
|
+
delta = -1 if event.num == 4 else 1
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
361
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
362
|
+
|
|
363
|
+
active_input_tab.text.yview_scroll(delta, "units")
|
|
364
|
+
active_output_tab.text.yview_scroll(delta, "units")
|
|
365
|
+
|
|
366
|
+
# Update line numbers if available
|
|
367
|
+
if hasattr(active_input_tab, '_on_text_modified'):
|
|
368
|
+
active_input_tab._on_text_modified()
|
|
369
|
+
if hasattr(active_output_tab, '_on_text_modified'):
|
|
370
|
+
active_output_tab._on_text_modified()
|
|
371
|
+
except (tk.TclError, IndexError):
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
return "break"
|
|
375
|
+
|
|
376
|
+
def _on_tab_content_changed(self, event=None):
|
|
377
|
+
"""Handle tab content changes."""
|
|
378
|
+
# Update tab labels when content changes
|
|
379
|
+
self.update_tab_labels()
|
|
380
|
+
|
|
381
|
+
# Update statistics
|
|
382
|
+
self.update_statistics()
|
|
383
|
+
|
|
384
|
+
# This can be overridden by the parent application
|
|
385
|
+
if hasattr(self, 'parent_callback') and self.parent_callback:
|
|
386
|
+
self.parent_callback()
|
|
387
|
+
|
|
388
|
+
def get_frame(self):
|
|
389
|
+
"""Return the main frame widget."""
|
|
390
|
+
return self.diff_frame
|
|
391
|
+
|
|
392
|
+
def show(self):
|
|
393
|
+
"""Show the diff viewer."""
|
|
394
|
+
self.diff_frame.grid(row=0, column=0, sticky="nsew", pady=5)
|
|
395
|
+
|
|
396
|
+
def hide(self):
|
|
397
|
+
"""Hide the diff viewer."""
|
|
398
|
+
self.diff_frame.grid_remove()
|
|
399
|
+
|
|
400
|
+
def load_content(self, input_tabs_content, output_tabs_content):
|
|
401
|
+
"""
|
|
402
|
+
Load content into the diff viewer tabs.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
input_tabs_content: List of strings for input tabs
|
|
406
|
+
output_tabs_content: List of strings for output tabs
|
|
407
|
+
"""
|
|
408
|
+
self.logger.info("Loading content into Diff Viewer.")
|
|
409
|
+
|
|
410
|
+
for i in range(min(len(input_tabs_content), self.tab_count)):
|
|
411
|
+
self.input_tabs[i].text.delete("1.0", tk.END)
|
|
412
|
+
self.input_tabs[i].text.insert("1.0", input_tabs_content[i])
|
|
413
|
+
|
|
414
|
+
for i in range(min(len(output_tabs_content), self.tab_count)):
|
|
415
|
+
self.output_tabs[i].text.delete("1.0", tk.END)
|
|
416
|
+
self.output_tabs[i].text.insert("1.0", output_tabs_content[i])
|
|
417
|
+
|
|
418
|
+
# Update tab labels after loading content
|
|
419
|
+
self.update_tab_labels()
|
|
420
|
+
|
|
421
|
+
def sync_content_back(self):
|
|
422
|
+
"""
|
|
423
|
+
Get content from diff viewer tabs.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
tuple: (input_contents, output_contents) as lists of strings
|
|
427
|
+
"""
|
|
428
|
+
self.logger.info("Syncing Diff Viewer content back.")
|
|
429
|
+
|
|
430
|
+
input_contents = []
|
|
431
|
+
output_contents = []
|
|
432
|
+
|
|
433
|
+
for i in range(self.tab_count):
|
|
434
|
+
input_content = self.input_tabs[i].text.get("1.0", tk.END)
|
|
435
|
+
# Remove trailing newline that tkinter adds
|
|
436
|
+
if input_content.endswith('\n'):
|
|
437
|
+
input_content = input_content[:-1]
|
|
438
|
+
input_contents.append(input_content)
|
|
439
|
+
|
|
440
|
+
output_content = self.output_tabs[i].text.get("1.0", tk.END)
|
|
441
|
+
# Remove trailing newline that tkinter adds
|
|
442
|
+
if output_content.endswith('\n'):
|
|
443
|
+
output_content = output_content[:-1]
|
|
444
|
+
output_contents.append(output_content)
|
|
445
|
+
|
|
446
|
+
# Debug logging
|
|
447
|
+
non_empty_inputs = sum(1 for content in input_contents if content.strip())
|
|
448
|
+
non_empty_outputs = sum(1 for content in output_contents if content.strip())
|
|
449
|
+
self.logger.info(f"Syncing back {non_empty_inputs} non-empty input tabs, {non_empty_outputs} non-empty output tabs")
|
|
450
|
+
|
|
451
|
+
return input_contents, output_contents
|
|
452
|
+
|
|
453
|
+
def _preprocess_for_diff(self, text, option):
|
|
454
|
+
"""
|
|
455
|
+
Preprocess text into line dicts according to diff option.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
text: Input text to preprocess
|
|
459
|
+
option: Comparison option ('ignore_case', 'match_case', 'ignore_whitespace')
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
List of dicts with 'raw' and 'cmp' keys
|
|
463
|
+
"""
|
|
464
|
+
lines = text.splitlines()
|
|
465
|
+
processed = []
|
|
466
|
+
for line in lines:
|
|
467
|
+
cmp_line = line
|
|
468
|
+
if option == "ignore_case":
|
|
469
|
+
cmp_line = cmp_line.lower()
|
|
470
|
+
elif option == "ignore_whitespace":
|
|
471
|
+
cmp_line = re.sub(r"\s+", " ", cmp_line).strip()
|
|
472
|
+
processed.append({"raw": line, "cmp": cmp_line})
|
|
473
|
+
return processed
|
|
474
|
+
|
|
475
|
+
def run_comparison(self, option=None):
|
|
476
|
+
"""
|
|
477
|
+
Compare the active tabs and display the diff.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
option: Comparison option ('ignore_case', 'match_case', 'ignore_whitespace')
|
|
481
|
+
If None, uses the current setting
|
|
482
|
+
"""
|
|
483
|
+
self.logger.info("Running Diff Viewer comparison.")
|
|
484
|
+
|
|
485
|
+
if option is not None:
|
|
486
|
+
self.settings["option"] = option
|
|
487
|
+
|
|
488
|
+
current_option = self.settings.get("option", "ignore_case")
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
active_input_idx = self.input_notebook.index("current")
|
|
492
|
+
active_output_idx = self.output_notebook.index("current")
|
|
493
|
+
|
|
494
|
+
input_widget = self.input_tabs[active_input_idx].text
|
|
495
|
+
output_widget = self.output_tabs[active_output_idx].text
|
|
496
|
+
|
|
497
|
+
# Get text content (remove trailing newline that tkinter adds)
|
|
498
|
+
input_text = input_widget.get("1.0", tk.END)
|
|
499
|
+
if input_text.endswith('\n'):
|
|
500
|
+
input_text = input_text[:-1]
|
|
501
|
+
|
|
502
|
+
output_text = output_widget.get("1.0", tk.END)
|
|
503
|
+
if output_text.endswith('\n'):
|
|
504
|
+
output_text = output_text[:-1]
|
|
505
|
+
|
|
506
|
+
except (tk.TclError, IndexError):
|
|
507
|
+
self.logger.error("Could not get active tabs for comparison")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
# Clear filters before comparison
|
|
511
|
+
if hasattr(self, 'input_filter_var'):
|
|
512
|
+
self.input_filter_var.set("")
|
|
513
|
+
if hasattr(self, 'output_filter_var'):
|
|
514
|
+
self.output_filter_var.set("")
|
|
515
|
+
|
|
516
|
+
# Clear stored original content
|
|
517
|
+
if active_input_idx in self.input_original_content:
|
|
518
|
+
del self.input_original_content[active_input_idx]
|
|
519
|
+
if active_output_idx in self.output_original_content:
|
|
520
|
+
del self.output_original_content[active_output_idx]
|
|
521
|
+
|
|
522
|
+
# Clear existing content
|
|
523
|
+
input_widget.delete("1.0", tk.END)
|
|
524
|
+
output_widget.delete("1.0", tk.END)
|
|
525
|
+
|
|
526
|
+
# Handle empty texts
|
|
527
|
+
if not input_text.strip() and not output_text.strip():
|
|
528
|
+
return
|
|
529
|
+
elif not input_text.strip():
|
|
530
|
+
for line in output_text.splitlines():
|
|
531
|
+
input_widget.insert(tk.END, '\n')
|
|
532
|
+
output_widget.insert(tk.END, line + '\n', 'addition')
|
|
533
|
+
return
|
|
534
|
+
elif not output_text.strip():
|
|
535
|
+
for line in input_text.splitlines():
|
|
536
|
+
input_widget.insert(tk.END, line + '\n', 'deletion')
|
|
537
|
+
output_widget.insert(tk.END, '\n')
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
# Preprocess texts for comparison
|
|
541
|
+
left_lines = self._preprocess_for_diff(input_text, current_option)
|
|
542
|
+
right_lines = self._preprocess_for_diff(output_text, current_option)
|
|
543
|
+
left_cmp = [l["cmp"] for l in left_lines]
|
|
544
|
+
right_cmp = [r["cmp"] for r in right_lines]
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
import difflib
|
|
548
|
+
matcher = difflib.SequenceMatcher(None, left_cmp, right_cmp, autojunk=False)
|
|
549
|
+
|
|
550
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
551
|
+
if tag == 'equal':
|
|
552
|
+
for i in range(i1, i2):
|
|
553
|
+
input_widget.insert(tk.END, left_lines[i]["raw"] + '\n')
|
|
554
|
+
output_widget.insert(tk.END, right_lines[j1 + (i - i1)]["raw"] + '\n')
|
|
555
|
+
|
|
556
|
+
elif tag == 'delete':
|
|
557
|
+
for i in range(i1, i2):
|
|
558
|
+
input_widget.insert(tk.END, left_lines[i]["raw"] + '\n', 'deletion')
|
|
559
|
+
output_widget.insert(tk.END, '\n')
|
|
560
|
+
|
|
561
|
+
elif tag == 'insert':
|
|
562
|
+
for j in range(j1, j2):
|
|
563
|
+
input_widget.insert(tk.END, '\n')
|
|
564
|
+
output_widget.insert(tk.END, right_lines[j]["raw"] + '\n', 'addition')
|
|
565
|
+
|
|
566
|
+
elif tag == 'replace':
|
|
567
|
+
input_block = [l["raw"] for l in left_lines[i1:i2]]
|
|
568
|
+
output_block = [r["raw"] for r in right_lines[j1:j2]]
|
|
569
|
+
|
|
570
|
+
# Pad blocks to same length
|
|
571
|
+
while len(input_block) < len(output_block):
|
|
572
|
+
input_block.append("")
|
|
573
|
+
while len(output_block) < len(input_block):
|
|
574
|
+
output_block.append("")
|
|
575
|
+
|
|
576
|
+
for line1, line2 in zip(input_block, output_block):
|
|
577
|
+
if line1 and line2:
|
|
578
|
+
self._highlight_word_diffs(input_widget, [line1], output_widget, [line2])
|
|
579
|
+
elif line1:
|
|
580
|
+
input_widget.insert(tk.END, line1 + '\n', 'deletion')
|
|
581
|
+
output_widget.insert(tk.END, '\n')
|
|
582
|
+
elif line2:
|
|
583
|
+
input_widget.insert(tk.END, '\n')
|
|
584
|
+
output_widget.insert(tk.END, line2 + '\n', 'addition')
|
|
585
|
+
|
|
586
|
+
except Exception as e:
|
|
587
|
+
self.logger.error(f"Error in diff computation: {e}")
|
|
588
|
+
input_widget.insert(tk.END, input_text)
|
|
589
|
+
output_widget.insert(tk.END, output_text)
|
|
590
|
+
|
|
591
|
+
# Reset scroll position
|
|
592
|
+
input_widget.yview_moveto(0)
|
|
593
|
+
output_widget.yview_moveto(0)
|
|
594
|
+
self._setup_sync()
|
|
595
|
+
|
|
596
|
+
# Update tab labels after comparison
|
|
597
|
+
self.update_tab_labels()
|
|
598
|
+
|
|
599
|
+
def _highlight_word_diffs(self, w1, lines1, w2, lines2):
|
|
600
|
+
"""
|
|
601
|
+
Highlight word-level differences within a 'replace' block.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
w1: First text widget
|
|
605
|
+
lines1: Lines for first widget
|
|
606
|
+
w2: Second text widget
|
|
607
|
+
lines2: Lines for second widget
|
|
608
|
+
"""
|
|
609
|
+
for line1, line2 in zip(lines1, lines2):
|
|
610
|
+
w1.insert(tk.END, line1 + '\n', 'modification')
|
|
611
|
+
w2.insert(tk.END, line2 + '\n', 'modification')
|
|
612
|
+
|
|
613
|
+
line_start1 = w1.index(f"{w1.index(tk.INSERT)} -1 lines linestart")
|
|
614
|
+
line_start2 = w2.index(f"{w2.index(tk.INSERT)} -1 lines linestart")
|
|
615
|
+
|
|
616
|
+
words1 = re.split(r'(\s+)', line1)
|
|
617
|
+
words2 = re.split(r'(\s+)', line2)
|
|
618
|
+
|
|
619
|
+
try:
|
|
620
|
+
import difflib
|
|
621
|
+
matcher = difflib.SequenceMatcher(None, words1, words2)
|
|
622
|
+
|
|
623
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
624
|
+
if tag == 'delete' or tag == 'replace':
|
|
625
|
+
start_char1 = len("".join(words1[:i1]))
|
|
626
|
+
end_char1 = len("".join(words1[:i2]))
|
|
627
|
+
w1.tag_add('inline_del', f"{line_start1}+{start_char1}c", f"{line_start1}+{end_char1}c")
|
|
628
|
+
if tag == 'insert' or tag == 'replace':
|
|
629
|
+
start_char2 = len("".join(words2[:j1]))
|
|
630
|
+
end_char2 = len("".join(words2[:j2]))
|
|
631
|
+
w2.tag_add('inline_add', f"{line_start2}+{start_char2}c", f"{line_start2}+{end_char2}c")
|
|
632
|
+
except Exception as e:
|
|
633
|
+
self.logger.error(f"Error in word-level diff highlighting: {e}")
|
|
634
|
+
|
|
635
|
+
def clear_all_input_tabs(self):
|
|
636
|
+
"""Clear all input tabs."""
|
|
637
|
+
for tab in self.input_tabs:
|
|
638
|
+
tab.text.delete("1.0", tk.END)
|
|
639
|
+
# Update tab labels after clearing
|
|
640
|
+
self.update_tab_labels()
|
|
641
|
+
|
|
642
|
+
def clear_all_output_tabs(self):
|
|
643
|
+
"""Clear all output tabs."""
|
|
644
|
+
for tab in self.output_tabs:
|
|
645
|
+
tab.text.delete("1.0", tk.END)
|
|
646
|
+
# Update tab labels after clearing
|
|
647
|
+
self.update_tab_labels()
|
|
648
|
+
|
|
649
|
+
def copy_to_clipboard(self):
|
|
650
|
+
"""Copy active output tab content to clipboard."""
|
|
651
|
+
try:
|
|
652
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
653
|
+
content = active_output_tab.text.get("1.0", tk.END)
|
|
654
|
+
self.parent.clipboard_clear()
|
|
655
|
+
self.parent.clipboard_append(content)
|
|
656
|
+
except (tk.TclError, IndexError):
|
|
657
|
+
pass
|
|
658
|
+
|
|
659
|
+
def copy_to_specific_input_tab(self, tab_index):
|
|
660
|
+
"""
|
|
661
|
+
Copy active output tab content to a specific input tab.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
tab_index: Index of the target input tab
|
|
665
|
+
"""
|
|
666
|
+
try:
|
|
667
|
+
active_output_tab = self.output_tabs[self.output_notebook.index("current")]
|
|
668
|
+
content = active_output_tab.text.get("1.0", tk.END)
|
|
669
|
+
|
|
670
|
+
if 0 <= tab_index < len(self.input_tabs):
|
|
671
|
+
self.input_tabs[tab_index].text.delete("1.0", tk.END)
|
|
672
|
+
self.input_tabs[tab_index].text.insert("1.0", content)
|
|
673
|
+
except (tk.TclError, IndexError):
|
|
674
|
+
pass
|
|
675
|
+
|
|
676
|
+
def load_file_to_input(self):
|
|
677
|
+
"""Load file content to the active input tab."""
|
|
678
|
+
try:
|
|
679
|
+
from tkinter import filedialog
|
|
680
|
+
file_path = filedialog.askopenfilename(
|
|
681
|
+
title="Select file to load",
|
|
682
|
+
filetypes=[
|
|
683
|
+
("Text files", "*.txt"),
|
|
684
|
+
("All files", "*.*")
|
|
685
|
+
],
|
|
686
|
+
parent=self.parent
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
if file_path:
|
|
690
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
691
|
+
content = f.read()
|
|
692
|
+
|
|
693
|
+
# Load into active input tab
|
|
694
|
+
active_input_tab = self.input_tabs[self.input_notebook.index("current")]
|
|
695
|
+
active_input_tab.text.delete("1.0", tk.END)
|
|
696
|
+
active_input_tab.text.insert("1.0", content)
|
|
697
|
+
|
|
698
|
+
self.logger.info(f"Loaded file: {file_path}")
|
|
699
|
+
except Exception as e:
|
|
700
|
+
self.logger.error(f"Error loading file: {e}")
|
|
701
|
+
self._show_error("Error", f"Could not load file: {str(e)}")
|
|
702
|
+
|
|
703
|
+
def update_tab_labels(self):
|
|
704
|
+
"""Update tab labels based on content."""
|
|
705
|
+
try:
|
|
706
|
+
# Update input tab labels
|
|
707
|
+
for i, tab in enumerate(self.input_tabs):
|
|
708
|
+
content = tab.text.get("1.0", tk.END).strip()
|
|
709
|
+
if content:
|
|
710
|
+
# Get first few words for the label
|
|
711
|
+
words = content.split()[:3]
|
|
712
|
+
label = " ".join(words)
|
|
713
|
+
if len(label) > 20:
|
|
714
|
+
label = label[:17] + "..."
|
|
715
|
+
if len(content.split()) > 3:
|
|
716
|
+
label += "..."
|
|
717
|
+
self.input_notebook.tab(i, text=f"{i+1}: {label}")
|
|
718
|
+
else:
|
|
719
|
+
self.input_notebook.tab(i, text=f"{i+1}:")
|
|
720
|
+
|
|
721
|
+
# Update output tab labels
|
|
722
|
+
for i, tab in enumerate(self.output_tabs):
|
|
723
|
+
content = tab.text.get("1.0", tk.END).strip()
|
|
724
|
+
if content:
|
|
725
|
+
# Get first few words for the label
|
|
726
|
+
words = content.split()[:3]
|
|
727
|
+
label = " ".join(words)
|
|
728
|
+
if len(label) > 20:
|
|
729
|
+
label = label[:17] + "..."
|
|
730
|
+
if len(content.split()) > 3:
|
|
731
|
+
label += "..."
|
|
732
|
+
self.output_notebook.tab(i, text=f"{i+1}: {label}")
|
|
733
|
+
else:
|
|
734
|
+
self.output_notebook.tab(i, text=f"{i+1}:")
|
|
735
|
+
|
|
736
|
+
except Exception as e:
|
|
737
|
+
self.logger.error(f"Error updating tab labels: {e}")
|
|
738
|
+
|
|
739
|
+
def _on_input_filter_changed(self, *args):
|
|
740
|
+
"""Handle input filter text changes."""
|
|
741
|
+
self._apply_input_filter()
|
|
742
|
+
|
|
743
|
+
def _on_output_filter_changed(self, *args):
|
|
744
|
+
"""Handle output filter text changes."""
|
|
745
|
+
self._apply_output_filter()
|
|
746
|
+
|
|
747
|
+
def _clear_input_filter(self):
|
|
748
|
+
"""Clear the input filter."""
|
|
749
|
+
self.input_filter_var.set("")
|
|
750
|
+
|
|
751
|
+
def _clear_output_filter(self):
|
|
752
|
+
"""Clear the output filter."""
|
|
753
|
+
self.output_filter_var.set("")
|
|
754
|
+
|
|
755
|
+
def _apply_input_filter(self):
|
|
756
|
+
"""Apply line filter to the active input tab."""
|
|
757
|
+
try:
|
|
758
|
+
active_idx = self.input_notebook.index("current")
|
|
759
|
+
current_tab = self.input_tabs[active_idx]
|
|
760
|
+
filter_text = self.input_filter_var.get().strip()
|
|
761
|
+
|
|
762
|
+
# Store original content if not already stored
|
|
763
|
+
if active_idx not in self.input_original_content:
|
|
764
|
+
self.input_original_content[active_idx] = current_tab.text.get("1.0", tk.END)
|
|
765
|
+
|
|
766
|
+
original_content = self.input_original_content[active_idx]
|
|
767
|
+
|
|
768
|
+
if filter_text:
|
|
769
|
+
# Apply filter
|
|
770
|
+
lines = original_content.split('\n')
|
|
771
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
772
|
+
filtered_content = '\n'.join(filtered_lines)
|
|
773
|
+
|
|
774
|
+
current_tab.text.delete("1.0", tk.END)
|
|
775
|
+
current_tab.text.insert("1.0", filtered_content)
|
|
776
|
+
else:
|
|
777
|
+
# Restore original content
|
|
778
|
+
current_tab.text.delete("1.0", tk.END)
|
|
779
|
+
current_tab.text.insert("1.0", original_content)
|
|
780
|
+
# Clear stored content
|
|
781
|
+
if active_idx in self.input_original_content:
|
|
782
|
+
del self.input_original_content[active_idx]
|
|
783
|
+
|
|
784
|
+
# Update statistics
|
|
785
|
+
self.update_statistics()
|
|
786
|
+
|
|
787
|
+
except Exception as e:
|
|
788
|
+
self.logger.error(f"Error applying input filter: {e}")
|
|
789
|
+
|
|
790
|
+
def _apply_output_filter(self):
|
|
791
|
+
"""Apply line filter to the active output tab."""
|
|
792
|
+
try:
|
|
793
|
+
active_idx = self.output_notebook.index("current")
|
|
794
|
+
current_tab = self.output_tabs[active_idx]
|
|
795
|
+
filter_text = self.output_filter_var.get().strip()
|
|
796
|
+
|
|
797
|
+
# Store original content if not already stored
|
|
798
|
+
if active_idx not in self.output_original_content:
|
|
799
|
+
self.output_original_content[active_idx] = current_tab.text.get("1.0", tk.END)
|
|
800
|
+
|
|
801
|
+
original_content = self.output_original_content[active_idx]
|
|
802
|
+
|
|
803
|
+
if filter_text:
|
|
804
|
+
# Apply filter
|
|
805
|
+
lines = original_content.split('\n')
|
|
806
|
+
filtered_lines = [line for line in lines if filter_text.lower() in line.lower()]
|
|
807
|
+
filtered_content = '\n'.join(filtered_lines)
|
|
808
|
+
|
|
809
|
+
current_tab.text.delete("1.0", tk.END)
|
|
810
|
+
current_tab.text.insert("1.0", filtered_content)
|
|
811
|
+
else:
|
|
812
|
+
# Restore original content
|
|
813
|
+
current_tab.text.delete("1.0", tk.END)
|
|
814
|
+
current_tab.text.insert("1.0", original_content)
|
|
815
|
+
# Clear stored content
|
|
816
|
+
if active_idx in self.output_original_content:
|
|
817
|
+
del self.output_original_content[active_idx]
|
|
818
|
+
|
|
819
|
+
# Update statistics
|
|
820
|
+
self.update_statistics()
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
self.logger.error(f"Error applying output filter: {e}")
|
|
824
|
+
|
|
825
|
+
def get_settings(self):
|
|
826
|
+
"""Get current diff viewer settings."""
|
|
827
|
+
return self.settings.copy()
|
|
828
|
+
|
|
829
|
+
def update_settings(self, settings):
|
|
830
|
+
"""
|
|
831
|
+
Update diff viewer settings.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
settings: Dictionary of settings to update
|
|
835
|
+
"""
|
|
836
|
+
self.settings.update(settings)
|
|
837
|
+
|
|
838
|
+
def apply_font_to_widgets(self, font_tuple):
|
|
839
|
+
"""
|
|
840
|
+
Apply font to all text widgets in the diff viewer.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
font_tuple: Tuple of (font_family, font_size)
|
|
844
|
+
"""
|
|
845
|
+
try:
|
|
846
|
+
for tab in self.input_tabs:
|
|
847
|
+
if hasattr(tab, 'text'):
|
|
848
|
+
tab.text.configure(font=font_tuple)
|
|
849
|
+
|
|
850
|
+
for tab in self.output_tabs:
|
|
851
|
+
if hasattr(tab, 'text'):
|
|
852
|
+
tab.text.configure(font=font_tuple)
|
|
853
|
+
|
|
854
|
+
self.logger.debug(f"Applied font {font_tuple} to diff viewer text widgets")
|
|
855
|
+
except Exception as e:
|
|
856
|
+
self.logger.error(f"Error applying font to diff viewer: {e}")
|
|
857
|
+
|
|
858
|
+
def update_statistics(self):
|
|
859
|
+
"""Update statistics bars for the active tabs."""
|
|
860
|
+
try:
|
|
861
|
+
# Get active tab indices
|
|
862
|
+
active_input_idx = self.input_notebook.index("current")
|
|
863
|
+
active_output_idx = self.output_notebook.index("current")
|
|
864
|
+
|
|
865
|
+
# Get text from active tabs
|
|
866
|
+
input_text = self.input_tabs[active_input_idx].text.get("1.0", tk.END)
|
|
867
|
+
output_text = self.output_tabs[active_output_idx].text.get("1.0", tk.END)
|
|
868
|
+
|
|
869
|
+
# Update input statistics
|
|
870
|
+
if self.input_stats_bar:
|
|
871
|
+
self._update_stats_bar(self.input_stats_bar, input_text)
|
|
872
|
+
|
|
873
|
+
# Update output statistics
|
|
874
|
+
if self.output_stats_bar:
|
|
875
|
+
self._update_stats_bar(self.output_stats_bar, output_text)
|
|
876
|
+
|
|
877
|
+
except Exception as e:
|
|
878
|
+
self.logger.error(f"Error updating statistics: {e}")
|
|
879
|
+
|
|
880
|
+
def _update_stats_bar(self, stats_bar, text):
|
|
881
|
+
"""
|
|
882
|
+
Update a statistics bar with text statistics.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
stats_bar: The label widget to update
|
|
886
|
+
text: The text to analyze
|
|
887
|
+
"""
|
|
888
|
+
try:
|
|
889
|
+
# Remove trailing newline that tkinter adds
|
|
890
|
+
if text.endswith('\n'):
|
|
891
|
+
text = text[:-1]
|
|
892
|
+
|
|
893
|
+
# Handle empty text
|
|
894
|
+
if not text:
|
|
895
|
+
stats_bar.config(text="Bytes: 0 | Word: 0 | Sentence: 0 | Line: 0 | Tokens: 0")
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
stripped_text = text.strip()
|
|
899
|
+
char_count = len(stripped_text)
|
|
900
|
+
byte_count = len(text.encode('utf-8'))
|
|
901
|
+
|
|
902
|
+
# Count lines (more accurate)
|
|
903
|
+
line_count = text.count('\n') + 1
|
|
904
|
+
|
|
905
|
+
# Count words
|
|
906
|
+
if char_count == 0:
|
|
907
|
+
word_count = 0
|
|
908
|
+
else:
|
|
909
|
+
words = [word for word in stripped_text.split() if word]
|
|
910
|
+
word_count = len(words)
|
|
911
|
+
|
|
912
|
+
# Count sentences (rough approximation)
|
|
913
|
+
sentence_count = text.count('.') + text.count('!') + text.count('?')
|
|
914
|
+
if sentence_count == 0 and char_count > 0:
|
|
915
|
+
sentence_count = 1
|
|
916
|
+
|
|
917
|
+
# Token estimation
|
|
918
|
+
token_count = max(1, round(char_count / 4)) if char_count > 0 else 0
|
|
919
|
+
|
|
920
|
+
# Format bytes
|
|
921
|
+
if byte_count < 1024:
|
|
922
|
+
formatted_bytes = f"{byte_count}"
|
|
923
|
+
elif byte_count < 1024 * 1024:
|
|
924
|
+
formatted_bytes = f"{byte_count / 1024:.1f}K"
|
|
925
|
+
else:
|
|
926
|
+
formatted_bytes = f"{byte_count / (1024 * 1024):.1f}M"
|
|
927
|
+
|
|
928
|
+
stats_bar.config(
|
|
929
|
+
text=f"Bytes: {formatted_bytes} | Word: {word_count} | Sentence: {sentence_count} | Line: {line_count} | Tokens: {token_count}"
|
|
930
|
+
)
|
|
931
|
+
except Exception as e:
|
|
932
|
+
self.logger.error(f"Error calculating statistics: {e}")
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
class DiffViewerSettingsWidget:
|
|
936
|
+
"""Settings widget for the diff viewer tool."""
|
|
937
|
+
|
|
938
|
+
def __init__(self, parent, diff_viewer, on_setting_change=None):
|
|
939
|
+
"""
|
|
940
|
+
Initialize the settings widget.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
parent: Parent tkinter widget
|
|
944
|
+
diff_viewer: DiffViewerWidget instance
|
|
945
|
+
on_setting_change: Callback function for setting changes
|
|
946
|
+
"""
|
|
947
|
+
self.parent = parent
|
|
948
|
+
self.diff_viewer = diff_viewer
|
|
949
|
+
self.on_setting_change = on_setting_change
|
|
950
|
+
|
|
951
|
+
# Get current settings
|
|
952
|
+
settings = diff_viewer.get_settings()
|
|
953
|
+
default_option = settings.get("option", "ignore_case")
|
|
954
|
+
|
|
955
|
+
# Create option variable
|
|
956
|
+
self.option_var = tk.StringVar(value=default_option)
|
|
957
|
+
|
|
958
|
+
# Create UI
|
|
959
|
+
self._create_ui()
|
|
960
|
+
|
|
961
|
+
def _create_ui(self):
|
|
962
|
+
"""Create the settings UI."""
|
|
963
|
+
ttk.Radiobutton(
|
|
964
|
+
self.parent,
|
|
965
|
+
text="Ignore case",
|
|
966
|
+
variable=self.option_var,
|
|
967
|
+
value="ignore_case",
|
|
968
|
+
command=self._on_option_change
|
|
969
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
970
|
+
|
|
971
|
+
ttk.Radiobutton(
|
|
972
|
+
self.parent,
|
|
973
|
+
text="Match case",
|
|
974
|
+
variable=self.option_var,
|
|
975
|
+
value="match_case",
|
|
976
|
+
command=self._on_option_change
|
|
977
|
+
).pack(side=tk.LEFT, padx=(0, 8))
|
|
978
|
+
|
|
979
|
+
ttk.Radiobutton(
|
|
980
|
+
self.parent,
|
|
981
|
+
text="Ignore whitespace",
|
|
982
|
+
variable=self.option_var,
|
|
983
|
+
value="ignore_whitespace",
|
|
984
|
+
command=self._on_option_change
|
|
985
|
+
).pack(side=tk.LEFT, padx=(0, 16))
|
|
986
|
+
|
|
987
|
+
ttk.Button(
|
|
988
|
+
self.parent,
|
|
989
|
+
text="Compare Active Tabs",
|
|
990
|
+
command=self._run_comparison
|
|
991
|
+
).pack(side=tk.LEFT, padx=5)
|
|
992
|
+
|
|
993
|
+
# Add separator
|
|
994
|
+
ttk.Label(self.parent, text="|").pack(side=tk.LEFT, padx=5)
|
|
995
|
+
|
|
996
|
+
ttk.Button(
|
|
997
|
+
self.parent,
|
|
998
|
+
text="List Comparator",
|
|
999
|
+
command=self._launch_list_comparator
|
|
1000
|
+
).pack(side=tk.LEFT, padx=5)
|
|
1001
|
+
|
|
1002
|
+
def _on_option_change(self):
|
|
1003
|
+
"""Handle option change."""
|
|
1004
|
+
option = self.option_var.get()
|
|
1005
|
+
self.diff_viewer.update_settings({"option": option})
|
|
1006
|
+
|
|
1007
|
+
if self.on_setting_change:
|
|
1008
|
+
self.on_setting_change("Diff Viewer", {"option": option})
|
|
1009
|
+
|
|
1010
|
+
def _run_comparison(self):
|
|
1011
|
+
"""Run the diff comparison."""
|
|
1012
|
+
option = self.option_var.get()
|
|
1013
|
+
self.diff_viewer.run_comparison(option)
|
|
1014
|
+
|
|
1015
|
+
def _launch_list_comparator(self):
|
|
1016
|
+
"""Launch the list comparator application."""
|
|
1017
|
+
try:
|
|
1018
|
+
# Try to use the parent app's integrated list comparator if available
|
|
1019
|
+
# Check on the diff_viewer instance (not self, which is the settings widget)
|
|
1020
|
+
if hasattr(self.diff_viewer, 'open_list_comparator') and callable(self.diff_viewer.open_list_comparator):
|
|
1021
|
+
self.diff_viewer.logger.info("✅ Found open_list_comparator method, calling it...")
|
|
1022
|
+
self.diff_viewer.open_list_comparator()
|
|
1023
|
+
self.diff_viewer.logger.info("✅ List Comparator launched via parent app")
|
|
1024
|
+
else:
|
|
1025
|
+
# Fallback: Launch as subprocess (standalone mode)
|
|
1026
|
+
import subprocess
|
|
1027
|
+
import os
|
|
1028
|
+
import sys
|
|
1029
|
+
|
|
1030
|
+
# Get the directory where the current script is located
|
|
1031
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
1032
|
+
list_comparator_path = os.path.join(current_dir, "list_comparator.py")
|
|
1033
|
+
|
|
1034
|
+
# Check if the list_comparator.py file exists
|
|
1035
|
+
if os.path.exists(list_comparator_path):
|
|
1036
|
+
# Launch the list comparator as a separate process without console window
|
|
1037
|
+
if sys.platform.startswith('win'):
|
|
1038
|
+
# Windows - use pythonw.exe to avoid console window, or hide it
|
|
1039
|
+
try:
|
|
1040
|
+
# Try to use pythonw.exe first (no console window)
|
|
1041
|
+
pythonw_path = sys.executable.replace('python.exe', 'pythonw.exe')
|
|
1042
|
+
if os.path.exists(pythonw_path):
|
|
1043
|
+
subprocess.Popen([pythonw_path, list_comparator_path])
|
|
1044
|
+
else:
|
|
1045
|
+
# Fallback: use regular python but hide the console window
|
|
1046
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
1047
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
1048
|
+
startupinfo.wShowWindow = subprocess.SW_HIDE
|
|
1049
|
+
subprocess.Popen([sys.executable, list_comparator_path],
|
|
1050
|
+
startupinfo=startupinfo)
|
|
1051
|
+
except:
|
|
1052
|
+
# Final fallback
|
|
1053
|
+
subprocess.Popen([sys.executable, list_comparator_path])
|
|
1054
|
+
else:
|
|
1055
|
+
# Unix/Linux/macOS
|
|
1056
|
+
subprocess.Popen([sys.executable, list_comparator_path])
|
|
1057
|
+
|
|
1058
|
+
print("✅ List Comparator launched successfully (subprocess)")
|
|
1059
|
+
else:
|
|
1060
|
+
print(f"❌ List Comparator not found at: {list_comparator_path}")
|
|
1061
|
+
# Try to show a message to the user if possible
|
|
1062
|
+
self._show_warning("List Comparator",
|
|
1063
|
+
f"List Comparator application not found.\n\nExpected location: {list_comparator_path}")
|
|
1064
|
+
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
print(f"❌ Error launching List Comparator: {e}")
|
|
1067
|
+
self._show_error("List Comparator",
|
|
1068
|
+
f"Error launching List Comparator:\n{str(e)}")
|
|
1069
|
+
|
|
1070
|
+
def get_settings(self):
|
|
1071
|
+
"""Get current settings."""
|
|
1072
|
+
return {"option": self.option_var.get()}
|