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.
Files changed (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +680 -0
  3. package/bin/pomera-ai-commander.js +62 -0
  4. package/core/__init__.py +66 -0
  5. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  7. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  8. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  9. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  10. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  11. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  12. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  13. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  14. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  15. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  16. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  17. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  18. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  19. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  20. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  21. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  22. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  23. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  24. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  25. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  26. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  27. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  28. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  29. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  30. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  31. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  32. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  33. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  34. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  35. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  36. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  37. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  38. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  39. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  40. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  41. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  42. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  43. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  44. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  45. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  46. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  47. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  48. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  49. package/core/app_context.py +482 -0
  50. package/core/async_text_processor.py +422 -0
  51. package/core/backup_manager.py +656 -0
  52. package/core/backup_recovery_manager.py +1034 -0
  53. package/core/content_hash_cache.py +509 -0
  54. package/core/context_menu.py +313 -0
  55. package/core/data_validator.py +1067 -0
  56. package/core/database_connection_manager.py +745 -0
  57. package/core/database_curl_settings_manager.py +609 -0
  58. package/core/database_promera_ai_settings_manager.py +447 -0
  59. package/core/database_schema.py +412 -0
  60. package/core/database_schema_manager.py +396 -0
  61. package/core/database_settings_manager.py +1508 -0
  62. package/core/database_settings_manager_interface.py +457 -0
  63. package/core/dialog_manager.py +735 -0
  64. package/core/efficient_line_numbers.py +511 -0
  65. package/core/error_handler.py +747 -0
  66. package/core/error_service.py +431 -0
  67. package/core/event_consolidator.py +512 -0
  68. package/core/mcp/__init__.py +43 -0
  69. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  70. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  71. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  72. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  73. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  74. package/core/mcp/protocol.py +288 -0
  75. package/core/mcp/schema.py +251 -0
  76. package/core/mcp/server_stdio.py +299 -0
  77. package/core/mcp/tool_registry.py +2345 -0
  78. package/core/memory_efficient_text_widget.py +712 -0
  79. package/core/migration_manager.py +915 -0
  80. package/core/migration_test_suite.py +1086 -0
  81. package/core/migration_validator.py +1144 -0
  82. package/core/optimized_find_replace.py +715 -0
  83. package/core/optimized_pattern_engine.py +424 -0
  84. package/core/optimized_search_highlighter.py +553 -0
  85. package/core/performance_monitor.py +675 -0
  86. package/core/persistence_manager.py +713 -0
  87. package/core/progressive_stats_calculator.py +632 -0
  88. package/core/regex_pattern_cache.py +530 -0
  89. package/core/regex_pattern_library.py +351 -0
  90. package/core/search_operation_manager.py +435 -0
  91. package/core/settings_defaults_registry.py +1087 -0
  92. package/core/settings_integrity_validator.py +1112 -0
  93. package/core/settings_serializer.py +558 -0
  94. package/core/settings_validator.py +1824 -0
  95. package/core/smart_stats_calculator.py +710 -0
  96. package/core/statistics_update_manager.py +619 -0
  97. package/core/stats_config_manager.py +858 -0
  98. package/core/streaming_text_handler.py +723 -0
  99. package/core/task_scheduler.py +596 -0
  100. package/core/update_pattern_library.py +169 -0
  101. package/core/visibility_monitor.py +596 -0
  102. package/core/widget_cache.py +498 -0
  103. package/mcp.json +61 -0
  104. package/package.json +57 -0
  105. package/pomera.py +7483 -0
  106. package/pomera_mcp_server.py +144 -0
  107. package/tools/__init__.py +5 -0
  108. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  110. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  111. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  112. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  113. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  114. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  115. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  116. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  117. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  118. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  119. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  120. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  121. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  122. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  123. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  124. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  125. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  126. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  127. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  128. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  129. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  130. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  131. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  132. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  133. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  134. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  135. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  136. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  137. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  138. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  139. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  140. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  141. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  142. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  143. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  144. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  145. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  146. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  147. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  148. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
  151. package/tools/ai_tools.py +2892 -0
  152. package/tools/ascii_art_generator.py +353 -0
  153. package/tools/base64_tools.py +184 -0
  154. package/tools/base_tool.py +511 -0
  155. package/tools/case_tool.py +309 -0
  156. package/tools/column_tools.py +396 -0
  157. package/tools/cron_tool.py +885 -0
  158. package/tools/curl_history.py +601 -0
  159. package/tools/curl_processor.py +1208 -0
  160. package/tools/curl_settings.py +503 -0
  161. package/tools/curl_tool.py +5467 -0
  162. package/tools/diff_viewer.py +1072 -0
  163. package/tools/email_extraction_tool.py +249 -0
  164. package/tools/email_header_analyzer.py +426 -0
  165. package/tools/extraction_tools.py +250 -0
  166. package/tools/find_replace.py +1751 -0
  167. package/tools/folder_file_reporter.py +1463 -0
  168. package/tools/folder_file_reporter_adapter.py +480 -0
  169. package/tools/generator_tools.py +1217 -0
  170. package/tools/hash_generator.py +256 -0
  171. package/tools/html_tool.py +657 -0
  172. package/tools/huggingface_helper.py +449 -0
  173. package/tools/jsonxml_tool.py +730 -0
  174. package/tools/line_tools.py +419 -0
  175. package/tools/list_comparator.py +720 -0
  176. package/tools/markdown_tools.py +562 -0
  177. package/tools/mcp_widget.py +1417 -0
  178. package/tools/notes_widget.py +973 -0
  179. package/tools/number_base_converter.py +373 -0
  180. package/tools/regex_extractor.py +572 -0
  181. package/tools/slug_generator.py +311 -0
  182. package/tools/sorter_tools.py +459 -0
  183. package/tools/string_escape_tool.py +393 -0
  184. package/tools/text_statistics_tool.py +366 -0
  185. package/tools/text_wrapper.py +431 -0
  186. package/tools/timestamp_converter.py +422 -0
  187. package/tools/tool_loader.py +710 -0
  188. package/tools/translator_tools.py +523 -0
  189. package/tools/url_link_extractor.py +262 -0
  190. package/tools/url_parser.py +205 -0
  191. package/tools/whitespace_tools.py +356 -0
  192. 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()