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,720 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import scrolledtext, messagebox, filedialog, ttk
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import csv
|
|
6
|
+
from itertools import zip_longest
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
# Import context menu support
|
|
10
|
+
try:
|
|
11
|
+
from core.context_menu import add_context_menu
|
|
12
|
+
CONTEXT_MENU_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
try:
|
|
15
|
+
from ..core.context_menu import add_context_menu
|
|
16
|
+
CONTEXT_MENU_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
CONTEXT_MENU_AVAILABLE = False
|
|
19
|
+
print("Context menu module not available")
|
|
20
|
+
|
|
21
|
+
class LineNumberText(tk.Frame):
|
|
22
|
+
"""A text widget with line numbers on the right side."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, parent, wrap=tk.WORD, width=40, height=10, state="normal", **kwargs):
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
|
|
27
|
+
# Create the main text widget
|
|
28
|
+
self.text = scrolledtext.ScrolledText(self, wrap=wrap, width=width, height=height, state=state, **kwargs)
|
|
29
|
+
self.text.pack(side="left", fill="both", expand=True)
|
|
30
|
+
|
|
31
|
+
# Create the line number widget
|
|
32
|
+
self.line_numbers = tk.Text(self, width=4, height=height, state="disabled",
|
|
33
|
+
bg=DiffApp.LINE_NUMBER_BG, fg=DiffApp.LINE_NUMBER_FG, relief="flat",
|
|
34
|
+
font=self.text.cget("font"))
|
|
35
|
+
self.line_numbers.pack(side="right", fill="y")
|
|
36
|
+
|
|
37
|
+
# Performance optimization: track last line count to avoid unnecessary updates
|
|
38
|
+
self._last_line_count = 0
|
|
39
|
+
self._update_pending = False
|
|
40
|
+
|
|
41
|
+
# Bind events to update line numbers
|
|
42
|
+
self.text.bind("<KeyRelease>", self._on_text_change)
|
|
43
|
+
self.text.bind("<Button-1>", self._on_text_change)
|
|
44
|
+
self.text.bind("<MouseWheel>", self._on_scroll)
|
|
45
|
+
self.text.bind("<Configure>", self._on_text_change)
|
|
46
|
+
|
|
47
|
+
# Bind scroll synchronization
|
|
48
|
+
self.text.bind("<MouseWheel>", self._on_scroll)
|
|
49
|
+
self.line_numbers.bind("<MouseWheel>", self._on_scroll)
|
|
50
|
+
|
|
51
|
+
# Initial line number update
|
|
52
|
+
self._update_line_numbers()
|
|
53
|
+
|
|
54
|
+
def _on_text_change(self, event=None):
|
|
55
|
+
"""Update line numbers when text changes."""
|
|
56
|
+
if not self._update_pending:
|
|
57
|
+
self._update_pending = True
|
|
58
|
+
self.after_idle(self._update_line_numbers)
|
|
59
|
+
return "break" if event and event.type == "2" else None # KeyPress events
|
|
60
|
+
|
|
61
|
+
def _on_scroll(self, event):
|
|
62
|
+
"""Handle scroll events and sync line numbers."""
|
|
63
|
+
# Sync scrolling between text and line numbers
|
|
64
|
+
if event.widget == self.text:
|
|
65
|
+
self.line_numbers.yview_moveto(self.text.yview()[0])
|
|
66
|
+
else:
|
|
67
|
+
self.text.yview_moveto(self.line_numbers.yview()[0])
|
|
68
|
+
return "break"
|
|
69
|
+
|
|
70
|
+
def _update_line_numbers(self):
|
|
71
|
+
"""Update the line numbers display efficiently."""
|
|
72
|
+
self._update_pending = False
|
|
73
|
+
|
|
74
|
+
# Get the number of lines
|
|
75
|
+
line_count = int(self.text.index("end-1c").split('.')[0])
|
|
76
|
+
|
|
77
|
+
# Only update if line count changed
|
|
78
|
+
if line_count != self._last_line_count:
|
|
79
|
+
self._last_line_count = line_count
|
|
80
|
+
|
|
81
|
+
# Create line number text more efficiently
|
|
82
|
+
if line_count > 0:
|
|
83
|
+
line_numbers_text = "\n".join(str(i) for i in range(1, line_count + 1))
|
|
84
|
+
else:
|
|
85
|
+
line_numbers_text = ""
|
|
86
|
+
|
|
87
|
+
# Update the line numbers widget
|
|
88
|
+
self.line_numbers.config(state="normal")
|
|
89
|
+
self.line_numbers.delete("1.0", tk.END)
|
|
90
|
+
if line_numbers_text:
|
|
91
|
+
self.line_numbers.insert("1.0", line_numbers_text)
|
|
92
|
+
self.line_numbers.config(state="disabled")
|
|
93
|
+
|
|
94
|
+
def get(self, index1, index2=None):
|
|
95
|
+
"""Delegate to the text widget's get method."""
|
|
96
|
+
return self.text.get(index1, index2)
|
|
97
|
+
|
|
98
|
+
def insert(self, index, string):
|
|
99
|
+
"""Delegate to the text widget's insert method."""
|
|
100
|
+
result = self.text.insert(index, string)
|
|
101
|
+
self._update_line_numbers()
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def delete(self, index1, index2=None):
|
|
105
|
+
"""Delegate to the text widget's delete method."""
|
|
106
|
+
result = self.text.delete(index1, index2)
|
|
107
|
+
self._update_line_numbers()
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
def config(self, **kwargs):
|
|
111
|
+
"""Delegate to the text widget's config method."""
|
|
112
|
+
return self.text.config(**kwargs)
|
|
113
|
+
|
|
114
|
+
def cget(self, key):
|
|
115
|
+
"""Delegate to the text widget's cget method."""
|
|
116
|
+
return self.text.cget(key)
|
|
117
|
+
|
|
118
|
+
class DiffApp:
|
|
119
|
+
SETTINGS_FILE = "settings.json"
|
|
120
|
+
|
|
121
|
+
# UI Constants
|
|
122
|
+
WINDOW_TITLE = "Advanced List Comparison Tool"
|
|
123
|
+
WINDOW_GEOMETRY = "1200x800"
|
|
124
|
+
TEXT_WIDGET_WIDTH = 40
|
|
125
|
+
TEXT_WIDGET_HEIGHT = 10
|
|
126
|
+
RESULT_WIDGET_WIDTH = 30
|
|
127
|
+
PATH_ENTRY_WIDTH = 50
|
|
128
|
+
|
|
129
|
+
# Colors and styling
|
|
130
|
+
LINE_NUMBER_BG = "#f0f0f0"
|
|
131
|
+
LINE_NUMBER_FG = "#666666"
|
|
132
|
+
|
|
133
|
+
def __init__(self, root, dialog_manager=None, send_to_input_callback=None):
|
|
134
|
+
self.root = root
|
|
135
|
+
self.dialog_manager = dialog_manager
|
|
136
|
+
self.send_to_input_callback = send_to_input_callback
|
|
137
|
+
self.root.title(self.WINDOW_TITLE)
|
|
138
|
+
self.root.geometry(self.WINDOW_GEOMETRY)
|
|
139
|
+
|
|
140
|
+
# --- Variables ---
|
|
141
|
+
self.case_insensitive = tk.BooleanVar()
|
|
142
|
+
self.output_path = tk.StringVar()
|
|
143
|
+
self.default_downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads')
|
|
144
|
+
if not os.path.exists(self.default_downloads_path):
|
|
145
|
+
self.default_downloads_path = os.path.expanduser('~') # Fallback to home dir
|
|
146
|
+
self.output_path.set(self.default_downloads_path)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --- Main Frames ---
|
|
150
|
+
top_frame = tk.Frame(root, pady=5)
|
|
151
|
+
top_frame.pack(fill="x", padx=10)
|
|
152
|
+
|
|
153
|
+
input_frame = tk.Frame(root)
|
|
154
|
+
input_frame.pack(pady=5, padx=10, fill="both", expand=True)
|
|
155
|
+
|
|
156
|
+
results_frame = tk.Frame(root)
|
|
157
|
+
results_frame.pack(pady=10, padx=10, fill="both", expand=True)
|
|
158
|
+
|
|
159
|
+
# --- Configuration Section ---
|
|
160
|
+
config_labelframe = tk.LabelFrame(top_frame, text="Configuration")
|
|
161
|
+
config_labelframe.pack(fill="x", expand="yes", side="left", padx=5)
|
|
162
|
+
|
|
163
|
+
case_checkbox = tk.Checkbutton(config_labelframe, text="Case Insensitive", variable=self.case_insensitive)
|
|
164
|
+
case_checkbox.pack(side="left", padx=5, pady=5)
|
|
165
|
+
|
|
166
|
+
tk.Label(config_labelframe, text="Output Path:").pack(side="left", padx=(10,0))
|
|
167
|
+
path_entry = tk.Entry(config_labelframe, textvariable=self.output_path, width=self.PATH_ENTRY_WIDTH)
|
|
168
|
+
path_entry.pack(side="left", fill="x", expand=True, padx=5)
|
|
169
|
+
select_path_button = tk.Button(config_labelframe, text="Select...", command=self.select_output_path)
|
|
170
|
+
select_path_button.pack(side="left", padx=5)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --- Buttons Section ---
|
|
174
|
+
button_frame = tk.Frame(top_frame)
|
|
175
|
+
button_frame.pack(side="right", padx=5)
|
|
176
|
+
|
|
177
|
+
compare_button = tk.Button(button_frame, text="Compare & Save", command=self.run_comparison_and_save)
|
|
178
|
+
compare_button.pack(side="left", padx=5)
|
|
179
|
+
|
|
180
|
+
self.export_button = tk.Button(button_frame, text="Export to CSV", command=self.export_to_csv, state="disabled")
|
|
181
|
+
self.export_button.pack(side="left", padx=5)
|
|
182
|
+
|
|
183
|
+
clear_button = tk.Button(button_frame, text="Clear All", command=self.clear_all_fields)
|
|
184
|
+
clear_button.pack(side="left", padx=5)
|
|
185
|
+
|
|
186
|
+
# Send to Input button (if callback is available)
|
|
187
|
+
print(f"[DEBUG] List Comparator: send_to_input_callback = {self.send_to_input_callback}")
|
|
188
|
+
if self.send_to_input_callback:
|
|
189
|
+
self.send_to_input_var = tk.StringVar(value="Send to Input")
|
|
190
|
+
self.send_to_input_menu = ttk.Menubutton(button_frame, textvariable=self.send_to_input_var, direction="below")
|
|
191
|
+
self.send_to_input_menu.pack(side="left", padx=5)
|
|
192
|
+
|
|
193
|
+
# Create the dropdown menu
|
|
194
|
+
self.send_dropdown_menu = tk.Menu(self.send_to_input_menu, tearoff=0)
|
|
195
|
+
self.send_to_input_menu.config(menu=self.send_dropdown_menu)
|
|
196
|
+
|
|
197
|
+
# Build the menu
|
|
198
|
+
self._build_send_to_input_menu()
|
|
199
|
+
|
|
200
|
+
# --- Input Lists Section ---
|
|
201
|
+
left_input_frame = tk.Frame(input_frame)
|
|
202
|
+
left_input_frame.pack(side="left", fill="both", expand=True, padx=(0, 5))
|
|
203
|
+
tk.Label(left_input_frame, text="List A").pack()
|
|
204
|
+
self.text_list_a = LineNumberText(left_input_frame, wrap=tk.WORD, width=self.TEXT_WIDGET_WIDTH, height=self.TEXT_WIDGET_HEIGHT)
|
|
205
|
+
self.text_list_a.pack(fill="both", expand=True)
|
|
206
|
+
# Stats bar for List A
|
|
207
|
+
self.stats_list_a = tk.Label(left_input_frame, text="Lines: 0 | Chars: 0", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
|
208
|
+
self.stats_list_a.pack(side="bottom", fill="x")
|
|
209
|
+
# Add context menu
|
|
210
|
+
if CONTEXT_MENU_AVAILABLE:
|
|
211
|
+
add_context_menu(self.text_list_a.text)
|
|
212
|
+
|
|
213
|
+
right_input_frame = tk.Frame(input_frame)
|
|
214
|
+
right_input_frame.pack(side="right", fill="both", expand=True, padx=(5, 0))
|
|
215
|
+
tk.Label(right_input_frame, text="List B").pack()
|
|
216
|
+
self.text_list_b = LineNumberText(right_input_frame, wrap=tk.WORD, width=self.TEXT_WIDGET_WIDTH, height=self.TEXT_WIDGET_HEIGHT)
|
|
217
|
+
self.text_list_b.pack(fill="both", expand=True)
|
|
218
|
+
# Stats bar for List B
|
|
219
|
+
self.stats_list_b = tk.Label(right_input_frame, text="Lines: 0 | Chars: 0", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
|
220
|
+
self.stats_list_b.pack(side="bottom", fill="x")
|
|
221
|
+
# Add context menu
|
|
222
|
+
if CONTEXT_MENU_AVAILABLE:
|
|
223
|
+
add_context_menu(self.text_list_b.text)
|
|
224
|
+
|
|
225
|
+
# --- Results Display Section ---
|
|
226
|
+
# --- Only in List A ---
|
|
227
|
+
left_result_frame = tk.Frame(results_frame)
|
|
228
|
+
left_result_frame.pack(side="left", fill="both", expand=True, padx=(0, 5))
|
|
229
|
+
tk.Label(left_result_frame, text="Only in List A").pack()
|
|
230
|
+
self.text_only_a = LineNumberText(left_result_frame, wrap=tk.WORD, width=self.RESULT_WIDGET_WIDTH, height=self.TEXT_WIDGET_HEIGHT, state="disabled")
|
|
231
|
+
self.text_only_a.pack(fill="both", expand=True)
|
|
232
|
+
self.status_only_a = tk.Label(left_result_frame, text="Count: 0", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
|
233
|
+
self.status_only_a.pack(side="bottom", fill="x")
|
|
234
|
+
# Add context menu
|
|
235
|
+
if CONTEXT_MENU_AVAILABLE:
|
|
236
|
+
add_context_menu(self.text_only_a.text)
|
|
237
|
+
|
|
238
|
+
# --- Only in List B ---
|
|
239
|
+
middle_result_frame = tk.Frame(results_frame)
|
|
240
|
+
middle_result_frame.pack(side="left", fill="both", expand=True, padx=5)
|
|
241
|
+
tk.Label(middle_result_frame, text="Only in List B").pack()
|
|
242
|
+
self.text_only_b = LineNumberText(middle_result_frame, wrap=tk.WORD, width=self.RESULT_WIDGET_WIDTH, height=self.TEXT_WIDGET_HEIGHT, state="disabled")
|
|
243
|
+
self.text_only_b.pack(fill="both", expand=True)
|
|
244
|
+
self.status_only_b = tk.Label(middle_result_frame, text="Count: 0", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
|
245
|
+
self.status_only_b.pack(side="bottom", fill="x")
|
|
246
|
+
# Add context menu
|
|
247
|
+
if CONTEXT_MENU_AVAILABLE:
|
|
248
|
+
add_context_menu(self.text_only_b.text)
|
|
249
|
+
|
|
250
|
+
# --- In Both Lists ---
|
|
251
|
+
right_result_frame = tk.Frame(results_frame)
|
|
252
|
+
right_result_frame.pack(side="left", fill="both", expand=True, padx=(5, 0))
|
|
253
|
+
tk.Label(right_result_frame, text="In Both Lists").pack()
|
|
254
|
+
self.text_in_both = LineNumberText(right_result_frame, wrap=tk.WORD, width=self.RESULT_WIDGET_WIDTH, height=self.TEXT_WIDGET_HEIGHT, state="disabled")
|
|
255
|
+
self.text_in_both.pack(fill="both", expand=True)
|
|
256
|
+
self.status_in_both = tk.Label(right_result_frame, text="Count: 0", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
|
257
|
+
self.status_in_both.pack(side="bottom", fill="x")
|
|
258
|
+
# Add context menu
|
|
259
|
+
if CONTEXT_MENU_AVAILABLE:
|
|
260
|
+
add_context_menu(self.text_in_both.text)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# --- Load settings on startup and save on exit ---
|
|
264
|
+
self.load_settings()
|
|
265
|
+
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
266
|
+
|
|
267
|
+
# Bind text change events to update stats
|
|
268
|
+
self.text_list_a.text.bind("<KeyRelease>", lambda e: self._update_stats(self.text_list_a, self.stats_list_a))
|
|
269
|
+
self.text_list_b.text.bind("<KeyRelease>", lambda e: self._update_stats(self.text_list_b, self.stats_list_b))
|
|
270
|
+
|
|
271
|
+
# Initial stats update
|
|
272
|
+
self._update_stats(self.text_list_a, self.stats_list_a)
|
|
273
|
+
self._update_stats(self.text_list_b, self.stats_list_b)
|
|
274
|
+
|
|
275
|
+
def _show_info(self, title, message, category="success"):
|
|
276
|
+
"""Show info dialog using DialogManager if available, otherwise use messagebox."""
|
|
277
|
+
if self.dialog_manager:
|
|
278
|
+
return self.dialog_manager.show_info(title, message, category, parent=self.root)
|
|
279
|
+
else:
|
|
280
|
+
messagebox.showinfo(title, message, parent=self.root)
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
def _show_warning(self, title, message, category="warning"):
|
|
284
|
+
"""Show warning dialog using DialogManager if available, otherwise use messagebox."""
|
|
285
|
+
if self.dialog_manager:
|
|
286
|
+
return self.dialog_manager.show_warning(title, message, category, parent=self.root)
|
|
287
|
+
else:
|
|
288
|
+
messagebox.showwarning(title, message, parent=self.root)
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
def _show_error(self, title, message):
|
|
292
|
+
"""Show error dialog using DialogManager if available, otherwise use messagebox."""
|
|
293
|
+
if self.dialog_manager:
|
|
294
|
+
return self.dialog_manager.show_error(title, message, parent=self.root)
|
|
295
|
+
else:
|
|
296
|
+
messagebox.showerror(title, message, parent=self.root)
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
def _ask_yes_no(self, title, message, category="confirmation"):
|
|
300
|
+
"""Show confirmation dialog using DialogManager if available, otherwise use messagebox."""
|
|
301
|
+
if self.dialog_manager:
|
|
302
|
+
return self.dialog_manager.ask_yes_no(title, message, category, parent=self.root)
|
|
303
|
+
else:
|
|
304
|
+
return messagebox.askyesno(title, message, parent=self.root)
|
|
305
|
+
|
|
306
|
+
def select_output_path(self):
|
|
307
|
+
"""Opens a dialog to select an output folder."""
|
|
308
|
+
path = filedialog.askdirectory(title="Select Output Folder", initialdir=self.output_path.get(), parent=self.root)
|
|
309
|
+
if path:
|
|
310
|
+
self.output_path.set(path)
|
|
311
|
+
|
|
312
|
+
def get_lists(self):
|
|
313
|
+
"""Gets the text from the input boxes and splits it into lines."""
|
|
314
|
+
try:
|
|
315
|
+
list1 = self.text_list_a.text.get("1.0", tk.END).strip().splitlines()
|
|
316
|
+
list2 = self.text_list_b.text.get("1.0", tk.END).strip().splitlines()
|
|
317
|
+
|
|
318
|
+
# Filter out empty lines and strip whitespace
|
|
319
|
+
list1 = [line.strip() for line in list1 if line.strip()]
|
|
320
|
+
list2 = [line.strip() for line in list2 if line.strip()]
|
|
321
|
+
|
|
322
|
+
return list1, list2
|
|
323
|
+
except Exception as e:
|
|
324
|
+
raise ValueError(f"Error reading input lists: {e}")
|
|
325
|
+
|
|
326
|
+
def run_comparison_and_save(self):
|
|
327
|
+
"""Compares the lists, displays results, and saves settings."""
|
|
328
|
+
try:
|
|
329
|
+
list1, list2 = self.get_lists()
|
|
330
|
+
|
|
331
|
+
# Perform comparison
|
|
332
|
+
results = self._compare_lists(list1, list2)
|
|
333
|
+
|
|
334
|
+
# Update UI with results
|
|
335
|
+
self._update_results_display(results)
|
|
336
|
+
|
|
337
|
+
# Update export button state
|
|
338
|
+
self._update_export_button_state(results)
|
|
339
|
+
|
|
340
|
+
# Save settings
|
|
341
|
+
self.save_settings()
|
|
342
|
+
self._show_info("Success", "Comparison complete and settings saved.")
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
self._show_error("Comparison Error", f"An error occurred during comparison:\n{e}")
|
|
346
|
+
|
|
347
|
+
def _compare_lists(self, list1, list2):
|
|
348
|
+
"""Compare two lists and return results dictionary."""
|
|
349
|
+
# Handle case-insensitivity more efficiently
|
|
350
|
+
if self.case_insensitive.get():
|
|
351
|
+
# Create case-insensitive sets while preserving original casing
|
|
352
|
+
set1_lower = {item.lower() for item in list1}
|
|
353
|
+
set2_lower = {item.lower() for item in list2}
|
|
354
|
+
|
|
355
|
+
# Create mapping from lowercase to original (preserving first occurrence)
|
|
356
|
+
map1 = {item.lower(): item for item in reversed(list1)}
|
|
357
|
+
map2 = {item.lower(): item for item in reversed(list2)}
|
|
358
|
+
|
|
359
|
+
# Find differences using lowercase sets
|
|
360
|
+
unique_to_a_lower = set1_lower - set2_lower
|
|
361
|
+
unique_to_b_lower = set2_lower - set1_lower
|
|
362
|
+
in_both_lower = set1_lower & set2_lower
|
|
363
|
+
|
|
364
|
+
# Map back to original casing
|
|
365
|
+
unique_to_a = sorted([map1[item] for item in unique_to_a_lower])
|
|
366
|
+
unique_to_b = sorted([map2[item] for item in unique_to_b_lower])
|
|
367
|
+
in_both = sorted([map1.get(item, map2.get(item)) for item in in_both_lower])
|
|
368
|
+
else:
|
|
369
|
+
# Case-sensitive comparison
|
|
370
|
+
set1, set2 = set(list1), set(list2)
|
|
371
|
+
unique_to_a = sorted(list(set1 - set2))
|
|
372
|
+
unique_to_b = sorted(list(set2 - set1))
|
|
373
|
+
in_both = sorted(list(set1 & set2))
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
'unique_to_a': unique_to_a,
|
|
377
|
+
'unique_to_b': unique_to_b,
|
|
378
|
+
'in_both': in_both
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def _update_results_display(self, results):
|
|
382
|
+
"""Update the result text widgets and status bars."""
|
|
383
|
+
# Update Only in List A
|
|
384
|
+
self.update_result_text(self.text_only_a, "\n".join(results['unique_to_a']))
|
|
385
|
+
self.status_only_a.config(text=f"Count: {len(results['unique_to_a'])}")
|
|
386
|
+
|
|
387
|
+
# Update Only in List B
|
|
388
|
+
self.update_result_text(self.text_only_b, "\n".join(results['unique_to_b']))
|
|
389
|
+
self.status_only_b.config(text=f"Count: {len(results['unique_to_b'])}")
|
|
390
|
+
|
|
391
|
+
# Update In Both Lists
|
|
392
|
+
self.update_result_text(self.text_in_both, "\n".join(results['in_both']))
|
|
393
|
+
self.status_in_both.config(text=f"Count: {len(results['in_both'])}")
|
|
394
|
+
|
|
395
|
+
def _update_export_button_state(self, results):
|
|
396
|
+
"""Update export button state based on results."""
|
|
397
|
+
has_results = any(results.values())
|
|
398
|
+
self.export_button.config(state="normal" if has_results else "disabled")
|
|
399
|
+
|
|
400
|
+
def export_to_csv(self):
|
|
401
|
+
"""Exports the content of all 5 text boxes to a CSV file."""
|
|
402
|
+
try:
|
|
403
|
+
output_dir = self.output_path.get().strip()
|
|
404
|
+
if not output_dir:
|
|
405
|
+
self._show_error("Error", "Output path is empty. Please select a valid output directory.")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
if not os.path.isdir(output_dir):
|
|
409
|
+
self._show_error("Error", f"Output path does not exist:\n{output_dir}")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Check if directory is writable
|
|
413
|
+
if not os.access(output_dir, os.W_OK):
|
|
414
|
+
self._show_error("Error", f"Output directory is not writable:\n{output_dir}")
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Generate unique filename if file already exists
|
|
418
|
+
base_filename = "comparison_results.csv"
|
|
419
|
+
output_file = os.path.join(output_dir, base_filename)
|
|
420
|
+
counter = 1
|
|
421
|
+
while os.path.exists(output_file):
|
|
422
|
+
name, ext = os.path.splitext(base_filename)
|
|
423
|
+
output_file = os.path.join(output_dir, f"{name}_{counter}{ext}")
|
|
424
|
+
counter += 1
|
|
425
|
+
|
|
426
|
+
# Get data from all 5 lists with validation
|
|
427
|
+
list_a = self._get_text_content(self.text_list_a)
|
|
428
|
+
list_b = self._get_text_content(self.text_list_b)
|
|
429
|
+
only_a = self._get_text_content(self.text_only_a)
|
|
430
|
+
only_b = self._get_text_content(self.text_only_b)
|
|
431
|
+
in_both = self._get_text_content(self.text_in_both)
|
|
432
|
+
|
|
433
|
+
# Use zip_longest to handle lists of different lengths
|
|
434
|
+
export_data = list(zip_longest(list_a, list_b, only_a, only_b, in_both, fillvalue=""))
|
|
435
|
+
|
|
436
|
+
headers = ["List A (Input)", "List B (Input)", "Only in List A", "Only in List B", "In Both Lists"]
|
|
437
|
+
|
|
438
|
+
with open(output_file, "w", newline="", encoding="utf-8") as f:
|
|
439
|
+
writer = csv.writer(f)
|
|
440
|
+
writer.writerow(headers)
|
|
441
|
+
writer.writerows(export_data)
|
|
442
|
+
|
|
443
|
+
self._show_info("Success", f"Data successfully exported to:\n{output_file}")
|
|
444
|
+
|
|
445
|
+
except PermissionError:
|
|
446
|
+
self._show_error("Export Failed", "Permission denied. The file may be open in another application.")
|
|
447
|
+
except OSError as e:
|
|
448
|
+
self._show_error("Export Failed", f"File system error: {e}")
|
|
449
|
+
except Exception as e:
|
|
450
|
+
self._show_error("Export Failed", f"An unexpected error occurred while exporting to CSV:\n{e}")
|
|
451
|
+
|
|
452
|
+
def _get_text_content(self, text_widget):
|
|
453
|
+
"""Safely get text content from a text widget."""
|
|
454
|
+
try:
|
|
455
|
+
content = text_widget.text.get("1.0", tk.END).strip()
|
|
456
|
+
return [line.strip() for line in content.splitlines() if line.strip()] if content else []
|
|
457
|
+
except Exception:
|
|
458
|
+
return []
|
|
459
|
+
|
|
460
|
+
def update_result_text(self, text_widget, content):
|
|
461
|
+
"""Helper function to update the read-only result widgets."""
|
|
462
|
+
text_widget.text.config(state="normal")
|
|
463
|
+
text_widget.text.delete("1.0", tk.END)
|
|
464
|
+
text_widget.text.insert("1.0", content)
|
|
465
|
+
text_widget.text.config(state="disabled")
|
|
466
|
+
|
|
467
|
+
def clear_all_fields(self):
|
|
468
|
+
"""Clears all input and result fields and resets status bars."""
|
|
469
|
+
self.text_list_a.text.delete("1.0", tk.END)
|
|
470
|
+
self.text_list_b.text.delete("1.0", tk.END)
|
|
471
|
+
self.update_result_text(self.text_only_a, "")
|
|
472
|
+
self.update_result_text(self.text_only_b, "")
|
|
473
|
+
self.update_result_text(self.text_in_both, "")
|
|
474
|
+
self.status_only_a.config(text="Count: 0")
|
|
475
|
+
self.status_only_b.config(text="Count: 0")
|
|
476
|
+
self.status_in_both.config(text="Count: 0")
|
|
477
|
+
self.export_button.config(state="disabled")
|
|
478
|
+
|
|
479
|
+
def save_settings(self):
|
|
480
|
+
"""Saves the content of all text boxes and config to a JSON file."""
|
|
481
|
+
try:
|
|
482
|
+
settings = {
|
|
483
|
+
"case_insensitive": self.case_insensitive.get(),
|
|
484
|
+
"output_path": self.output_path.get().strip(),
|
|
485
|
+
"list_a": self.text_list_a.text.get("1.0", tk.END).strip(),
|
|
486
|
+
"list_b": self.text_list_b.text.get("1.0", tk.END).strip(),
|
|
487
|
+
"only_a": self.text_only_a.text.get("1.0", tk.END).strip(),
|
|
488
|
+
"only_b": self.text_only_b.text.get("1.0", tk.END).strip(),
|
|
489
|
+
"in_both": self.text_in_both.text.get("1.0", tk.END).strip()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# Validate settings before saving
|
|
493
|
+
if not self._validate_settings(settings):
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
with open(self.SETTINGS_FILE, "w", encoding="utf-8") as f:
|
|
497
|
+
json.dump(settings, f, indent=4, ensure_ascii=False)
|
|
498
|
+
return True
|
|
499
|
+
|
|
500
|
+
except PermissionError:
|
|
501
|
+
print(f"Permission denied: Cannot save settings to {self.SETTINGS_FILE}")
|
|
502
|
+
return False
|
|
503
|
+
except OSError as e:
|
|
504
|
+
print(f"File system error saving settings: {e}")
|
|
505
|
+
return False
|
|
506
|
+
except Exception as e:
|
|
507
|
+
print(f"Unexpected error saving settings: {e}")
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
def _validate_settings(self, settings):
|
|
511
|
+
"""Validate settings data before saving."""
|
|
512
|
+
try:
|
|
513
|
+
# Check if output path is valid
|
|
514
|
+
output_path = settings.get("output_path", "")
|
|
515
|
+
if output_path and not os.path.isdir(output_path):
|
|
516
|
+
print(f"Warning: Output path does not exist: {output_path}")
|
|
517
|
+
# Don't fail validation, just warn
|
|
518
|
+
|
|
519
|
+
# Check if boolean values are valid
|
|
520
|
+
if not isinstance(settings.get("case_insensitive"), bool):
|
|
521
|
+
print("Warning: Invalid case_insensitive value")
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
return True
|
|
525
|
+
except Exception as e:
|
|
526
|
+
print(f"Settings validation error: {e}")
|
|
527
|
+
return False
|
|
528
|
+
|
|
529
|
+
def load_settings(self):
|
|
530
|
+
"""Loads settings from the JSON file if it exists."""
|
|
531
|
+
if not os.path.exists(self.SETTINGS_FILE):
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
with open(self.SETTINGS_FILE, "r", encoding="utf-8") as f:
|
|
536
|
+
settings = json.load(f)
|
|
537
|
+
|
|
538
|
+
# Load configuration settings with validation
|
|
539
|
+
self.case_insensitive.set(settings.get("case_insensitive", False))
|
|
540
|
+
|
|
541
|
+
output_path = settings.get("output_path", "").strip()
|
|
542
|
+
if output_path and os.path.isdir(output_path):
|
|
543
|
+
self.output_path.set(output_path)
|
|
544
|
+
else:
|
|
545
|
+
self.output_path.set(self.default_downloads_path)
|
|
546
|
+
|
|
547
|
+
# Load text content safely
|
|
548
|
+
self._load_text_content(self.text_list_a, settings.get("list_a", ""))
|
|
549
|
+
self._load_text_content(self.text_list_b, settings.get("list_b", ""))
|
|
550
|
+
|
|
551
|
+
# Process result fields, handling old format with counts
|
|
552
|
+
self.process_loaded_result(self.text_only_a, self.status_only_a, settings.get("only_a", ""))
|
|
553
|
+
self.process_loaded_result(self.text_only_b, self.status_only_b, settings.get("only_b", ""))
|
|
554
|
+
self.process_loaded_result(self.text_in_both, self.status_in_both, settings.get("in_both", ""))
|
|
555
|
+
|
|
556
|
+
# Enable export button if there are results
|
|
557
|
+
if settings.get("only_a") or settings.get("only_b") or settings.get("in_both"):
|
|
558
|
+
self.export_button.config(state="normal")
|
|
559
|
+
|
|
560
|
+
except json.JSONDecodeError as e:
|
|
561
|
+
print(f"Invalid JSON in settings file: {e}")
|
|
562
|
+
except PermissionError:
|
|
563
|
+
print(f"Permission denied: Cannot read settings file {self.SETTINGS_FILE}")
|
|
564
|
+
except OSError as e:
|
|
565
|
+
print(f"File system error loading settings: {e}")
|
|
566
|
+
except Exception as e:
|
|
567
|
+
print(f"Unexpected error loading settings: {e}")
|
|
568
|
+
|
|
569
|
+
def _load_text_content(self, text_widget, content):
|
|
570
|
+
"""Safely load text content into a text widget."""
|
|
571
|
+
try:
|
|
572
|
+
if content and isinstance(content, str):
|
|
573
|
+
text_widget.text.insert("1.0", content)
|
|
574
|
+
except Exception as e:
|
|
575
|
+
print(f"Error loading text content: {e}")
|
|
576
|
+
|
|
577
|
+
def process_loaded_result(self, text_widget, status_label, content):
|
|
578
|
+
"""Processes loaded result content, separating count from list for backward compatibility."""
|
|
579
|
+
lines = content.splitlines()
|
|
580
|
+
# Check if the first line matches the old "Count: X" format
|
|
581
|
+
if lines and re.match(r'^Count: \d+$', lines[0]):
|
|
582
|
+
status_label.config(text=lines[0])
|
|
583
|
+
# The list content starts after the count and a blank line
|
|
584
|
+
list_content = "\n".join(lines[2:])
|
|
585
|
+
self.update_result_text(text_widget, list_content)
|
|
586
|
+
else:
|
|
587
|
+
# New format or empty, just load content and update count based on lines
|
|
588
|
+
self.update_result_text(text_widget, content)
|
|
589
|
+
item_count = len(lines) if content else 0
|
|
590
|
+
status_label.config(text=f"Count: {item_count}")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def on_closing(self):
|
|
594
|
+
"""Handles the window closing event."""
|
|
595
|
+
self.save_settings()
|
|
596
|
+
self.root.destroy()
|
|
597
|
+
|
|
598
|
+
def _update_stats(self, text_widget, stats_label):
|
|
599
|
+
"""Update stats bar with line and character counts."""
|
|
600
|
+
try:
|
|
601
|
+
content = text_widget.text.get("1.0", tk.END)
|
|
602
|
+
# Count lines (excluding the final empty line that tkinter adds)
|
|
603
|
+
lines = content.splitlines()
|
|
604
|
+
line_count = len([line for line in lines if line.strip()])
|
|
605
|
+
# Count characters (excluding trailing newline)
|
|
606
|
+
char_count = len(content.rstrip('\n'))
|
|
607
|
+
stats_label.config(text=f"Lines: {line_count} | Chars: {char_count}")
|
|
608
|
+
except Exception as e:
|
|
609
|
+
print(f"Error updating stats: {e}")
|
|
610
|
+
|
|
611
|
+
def _build_send_to_input_menu(self):
|
|
612
|
+
"""Build the Send to Input dropdown menu."""
|
|
613
|
+
try:
|
|
614
|
+
# Clear existing menu
|
|
615
|
+
self.send_dropdown_menu.delete(0, tk.END)
|
|
616
|
+
|
|
617
|
+
# Add options for each text area
|
|
618
|
+
self.send_dropdown_menu.add_command(
|
|
619
|
+
label="List A → Input Tab 1",
|
|
620
|
+
command=lambda: self._send_content_to_input(0, self.text_list_a)
|
|
621
|
+
)
|
|
622
|
+
self.send_dropdown_menu.add_command(
|
|
623
|
+
label="List B → Input Tab 2",
|
|
624
|
+
command=lambda: self._send_content_to_input(1, self.text_list_b)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
self.send_dropdown_menu.add_separator()
|
|
628
|
+
|
|
629
|
+
self.send_dropdown_menu.add_command(
|
|
630
|
+
label="Only in A → Input Tab 3",
|
|
631
|
+
command=lambda: self._send_content_to_input(2, self.text_only_a)
|
|
632
|
+
)
|
|
633
|
+
self.send_dropdown_menu.add_command(
|
|
634
|
+
label="Only in B → Input Tab 4",
|
|
635
|
+
command=lambda: self._send_content_to_input(3, self.text_only_b)
|
|
636
|
+
)
|
|
637
|
+
self.send_dropdown_menu.add_command(
|
|
638
|
+
label="In Both → Input Tab 5",
|
|
639
|
+
command=lambda: self._send_content_to_input(4, self.text_in_both)
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
self.send_dropdown_menu.add_separator()
|
|
643
|
+
|
|
644
|
+
# Add option to send all results
|
|
645
|
+
self.send_dropdown_menu.add_command(
|
|
646
|
+
label="All Results → Input Tab 6",
|
|
647
|
+
command=self._send_all_results_to_input
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
except Exception as e:
|
|
651
|
+
print(f"Error building send to input menu: {e}")
|
|
652
|
+
|
|
653
|
+
def _send_content_to_input(self, tab_index, text_widget):
|
|
654
|
+
"""Send content from a text widget to an input tab."""
|
|
655
|
+
try:
|
|
656
|
+
if not self.send_to_input_callback:
|
|
657
|
+
self._show_warning("Warning", "Send to Input functionality is not available.")
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# Get content from text widget
|
|
661
|
+
content = text_widget.text.get("1.0", tk.END).strip()
|
|
662
|
+
|
|
663
|
+
if not content:
|
|
664
|
+
self._show_warning("No Content", "The selected text area is empty.")
|
|
665
|
+
return
|
|
666
|
+
|
|
667
|
+
# Send to input tab using callback
|
|
668
|
+
self.send_to_input_callback(tab_index, content)
|
|
669
|
+
|
|
670
|
+
# Show success message
|
|
671
|
+
self._show_info("Success", f"Content sent to Input Tab {tab_index + 1}", category="success")
|
|
672
|
+
|
|
673
|
+
except Exception as e:
|
|
674
|
+
self._show_error("Error", f"Failed to send content to input:\n{str(e)}")
|
|
675
|
+
|
|
676
|
+
def _send_all_results_to_input(self):
|
|
677
|
+
"""Send all comparison results to an input tab."""
|
|
678
|
+
try:
|
|
679
|
+
if not self.send_to_input_callback:
|
|
680
|
+
self._show_warning("Warning", "Send to Input functionality is not available.")
|
|
681
|
+
return
|
|
682
|
+
|
|
683
|
+
# Build combined results
|
|
684
|
+
only_a = self.text_only_a.text.get("1.0", tk.END).strip()
|
|
685
|
+
only_b = self.text_only_b.text.get("1.0", tk.END).strip()
|
|
686
|
+
in_both = self.text_in_both.text.get("1.0", tk.END).strip()
|
|
687
|
+
|
|
688
|
+
if not any([only_a, only_b, in_both]):
|
|
689
|
+
self._show_warning("No Results", "No comparison results to send. Run a comparison first.")
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
# Format combined results
|
|
693
|
+
combined = []
|
|
694
|
+
if only_a:
|
|
695
|
+
combined.append("=== Only in List A ===")
|
|
696
|
+
combined.append(only_a)
|
|
697
|
+
combined.append("")
|
|
698
|
+
if only_b:
|
|
699
|
+
combined.append("=== Only in List B ===")
|
|
700
|
+
combined.append(only_b)
|
|
701
|
+
combined.append("")
|
|
702
|
+
if in_both:
|
|
703
|
+
combined.append("=== In Both Lists ===")
|
|
704
|
+
combined.append(in_both)
|
|
705
|
+
|
|
706
|
+
content = "\n".join(combined)
|
|
707
|
+
|
|
708
|
+
# Send to input tab 6
|
|
709
|
+
self.send_to_input_callback(5, content)
|
|
710
|
+
|
|
711
|
+
# Show success message
|
|
712
|
+
self._show_info("Success", "All results sent to Input Tab 6", category="success")
|
|
713
|
+
|
|
714
|
+
except Exception as e:
|
|
715
|
+
self._show_error("Error", f"Failed to send results to input:\n{str(e)}")
|
|
716
|
+
|
|
717
|
+
if __name__ == "__main__":
|
|
718
|
+
root = tk.Tk()
|
|
719
|
+
app = DiffApp(root)
|
|
720
|
+
root.mainloop()
|