pomera-ai-commander 0.1.0 → 1.2.1

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.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,1072 +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."""
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
1072
  return {"option": self.option_var.get()}