pomera-ai-commander 1.1.1 → 1.2.2

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 (213) 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 +1199 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_directory.py +549 -0
  12. package/core/data_validator.py +1066 -1066
  13. package/core/database_connection_manager.py +744 -744
  14. package/core/database_curl_settings_manager.py +608 -608
  15. package/core/database_promera_ai_settings_manager.py +446 -446
  16. package/core/database_schema.py +411 -411
  17. package/core/database_schema_manager.py +395 -395
  18. package/core/database_settings_manager.py +1507 -1507
  19. package/core/database_settings_manager_interface.py +456 -456
  20. package/core/dialog_manager.py +734 -734
  21. package/core/diff_utils.py +239 -0
  22. package/core/efficient_line_numbers.py +540 -510
  23. package/core/error_handler.py +746 -746
  24. package/core/error_service.py +431 -431
  25. package/core/event_consolidator.py +511 -511
  26. package/core/mcp/__init__.py +43 -43
  27. package/core/mcp/find_replace_diff.py +334 -0
  28. package/core/mcp/protocol.py +288 -288
  29. package/core/mcp/schema.py +251 -251
  30. package/core/mcp/server_stdio.py +299 -299
  31. package/core/mcp/tool_registry.py +2699 -2345
  32. package/core/memento.py +275 -0
  33. package/core/memory_efficient_text_widget.py +711 -711
  34. package/core/migration_manager.py +914 -914
  35. package/core/migration_test_suite.py +1085 -1085
  36. package/core/migration_validator.py +1143 -1143
  37. package/core/optimized_find_replace.py +714 -714
  38. package/core/optimized_pattern_engine.py +424 -424
  39. package/core/optimized_search_highlighter.py +552 -552
  40. package/core/performance_monitor.py +674 -674
  41. package/core/persistence_manager.py +712 -712
  42. package/core/progressive_stats_calculator.py +632 -632
  43. package/core/regex_pattern_cache.py +529 -529
  44. package/core/regex_pattern_library.py +350 -350
  45. package/core/search_operation_manager.py +434 -434
  46. package/core/settings_defaults_registry.py +1087 -1087
  47. package/core/settings_integrity_validator.py +1111 -1111
  48. package/core/settings_serializer.py +557 -557
  49. package/core/settings_validator.py +1823 -1823
  50. package/core/smart_stats_calculator.py +709 -709
  51. package/core/statistics_update_manager.py +619 -619
  52. package/core/stats_config_manager.py +858 -858
  53. package/core/streaming_text_handler.py +723 -723
  54. package/core/task_scheduler.py +596 -596
  55. package/core/update_pattern_library.py +168 -168
  56. package/core/visibility_monitor.py +596 -596
  57. package/core/widget_cache.py +498 -498
  58. package/mcp.json +51 -61
  59. package/migrate_data.py +127 -0
  60. package/package.json +64 -57
  61. package/pomera.py +7883 -7482
  62. package/pomera_mcp_server.py +183 -144
  63. package/requirements.txt +33 -0
  64. package/scripts/Dockerfile.alpine +43 -0
  65. package/scripts/Dockerfile.gui-test +54 -0
  66. package/scripts/Dockerfile.linux +43 -0
  67. package/scripts/Dockerfile.test-linux +80 -0
  68. package/scripts/Dockerfile.ubuntu +39 -0
  69. package/scripts/README.md +53 -0
  70. package/scripts/build-all.bat +113 -0
  71. package/scripts/build-docker.bat +53 -0
  72. package/scripts/build-docker.sh +55 -0
  73. package/scripts/build-optimized.bat +101 -0
  74. package/scripts/build.sh +78 -0
  75. package/scripts/docker-compose.test.yml +27 -0
  76. package/scripts/docker-compose.yml +32 -0
  77. package/scripts/postinstall.js +62 -0
  78. package/scripts/requirements-minimal.txt +33 -0
  79. package/scripts/test-linux-simple.bat +28 -0
  80. package/scripts/validate-release-workflow.py +450 -0
  81. package/tools/__init__.py +4 -4
  82. package/tools/ai_tools.py +2891 -2891
  83. package/tools/ascii_art_generator.py +352 -352
  84. package/tools/base64_tools.py +183 -183
  85. package/tools/base_tool.py +511 -511
  86. package/tools/case_tool.py +308 -308
  87. package/tools/column_tools.py +395 -395
  88. package/tools/cron_tool.py +884 -884
  89. package/tools/curl_history.py +600 -600
  90. package/tools/curl_processor.py +1207 -1207
  91. package/tools/curl_settings.py +502 -502
  92. package/tools/curl_tool.py +5467 -5467
  93. package/tools/diff_viewer.py +1817 -1072
  94. package/tools/email_extraction_tool.py +248 -248
  95. package/tools/email_header_analyzer.py +425 -425
  96. package/tools/extraction_tools.py +250 -250
  97. package/tools/find_replace.py +2289 -1750
  98. package/tools/folder_file_reporter.py +1463 -1463
  99. package/tools/folder_file_reporter_adapter.py +480 -480
  100. package/tools/generator_tools.py +1216 -1216
  101. package/tools/hash_generator.py +255 -255
  102. package/tools/html_tool.py +656 -656
  103. package/tools/jsonxml_tool.py +729 -729
  104. package/tools/line_tools.py +419 -419
  105. package/tools/markdown_tools.py +561 -561
  106. package/tools/mcp_widget.py +1417 -1417
  107. package/tools/notes_widget.py +978 -973
  108. package/tools/number_base_converter.py +372 -372
  109. package/tools/regex_extractor.py +571 -571
  110. package/tools/slug_generator.py +310 -310
  111. package/tools/sorter_tools.py +458 -458
  112. package/tools/string_escape_tool.py +392 -392
  113. package/tools/text_statistics_tool.py +365 -365
  114. package/tools/text_wrapper.py +430 -430
  115. package/tools/timestamp_converter.py +421 -421
  116. package/tools/tool_loader.py +710 -710
  117. package/tools/translator_tools.py +522 -522
  118. package/tools/url_link_extractor.py +261 -261
  119. package/tools/url_parser.py +204 -204
  120. package/tools/whitespace_tools.py +355 -355
  121. package/tools/word_frequency_counter.py +146 -146
  122. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  123. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  124. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  125. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  126. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  127. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  128. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  129. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  131. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  132. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  134. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  135. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  136. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  137. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  138. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  139. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  140. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  141. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  142. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  143. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  144. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  145. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  146. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  147. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  148. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  149. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  150. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  151. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  152. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  153. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  154. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  155. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  156. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  157. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  158. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  159. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  160. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  161. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  162. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  163. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  164. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  165. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  166. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  167. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  168. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  169. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  170. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  192. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  193. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  194. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  195. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  196. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  197. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  198. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  199. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  200. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  201. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  202. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  203. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  204. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  205. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  206. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  207. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  208. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  209. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  210. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  211. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  212. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  213. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,419 +1,419 @@
1
- """
2
- Line Tools Module - Line manipulation utilities
3
-
4
- This module provides comprehensive line manipulation functionality with a tabbed UI interface
5
- for the Pomera AI Commander application.
6
-
7
- Features:
8
- - Remove Duplicates: Remove duplicate lines (keep first/last, case-sensitive option)
9
- - Remove Empty Lines: Remove blank/whitespace-only lines
10
- - Add Line Numbers: Prefix lines with numbers
11
- - Remove Line Numbers: Strip leading numbers from lines
12
- - Reverse Lines: Reverse line order
13
- - Shuffle Lines: Randomize line order
14
- """
15
-
16
- import tkinter as tk
17
- from tkinter import ttk
18
- import random
19
- import re
20
-
21
-
22
- class LineToolsProcessor:
23
- """Line tools processor with various line manipulation capabilities."""
24
-
25
- @staticmethod
26
- def remove_duplicates(text, mode="keep_first", case_sensitive=True):
27
- """Remove duplicate lines from text."""
28
- lines = text.splitlines()
29
-
30
- if mode == "keep_first":
31
- seen = set()
32
- result = []
33
- for line in lines:
34
- key = line if case_sensitive else line.lower()
35
- if key not in seen:
36
- seen.add(key)
37
- result.append(line)
38
- else: # keep_last
39
- seen = {}
40
- for i, line in enumerate(lines):
41
- key = line if case_sensitive else line.lower()
42
- seen[key] = (i, line)
43
- result = [v[1] for v in sorted(seen.values(), key=lambda x: x[0])]
44
-
45
- return '\n'.join(result)
46
-
47
- @staticmethod
48
- def remove_empty_lines(text, preserve_single=False):
49
- """Remove empty or whitespace-only lines."""
50
- lines = text.splitlines()
51
-
52
- if preserve_single:
53
- result = []
54
- prev_empty = False
55
- for line in lines:
56
- is_empty = not line.strip()
57
- if is_empty:
58
- if not prev_empty:
59
- result.append('')
60
- prev_empty = True
61
- else:
62
- result.append(line)
63
- prev_empty = False
64
- else:
65
- result = [line for line in lines if line.strip()]
66
-
67
- return '\n'.join(result)
68
-
69
- @staticmethod
70
- def add_line_numbers(text, format_style="1. ", start_number=1, skip_empty=False):
71
- """Add line numbers to each line."""
72
- lines = text.splitlines()
73
- result = []
74
- num = start_number
75
-
76
- for line in lines:
77
- if skip_empty and not line.strip():
78
- result.append(line)
79
- else:
80
- if format_style == "1. ":
81
- prefix = f"{num}. "
82
- elif format_style == "1) ":
83
- prefix = f"{num}) "
84
- elif format_style == "[1] ":
85
- prefix = f"[{num}] "
86
- elif format_style == "1: ":
87
- prefix = f"{num}: "
88
- else:
89
- prefix = f"{num}. "
90
- result.append(f"{prefix}{line}")
91
- num += 1
92
-
93
- return '\n'.join(result)
94
-
95
- @staticmethod
96
- def remove_line_numbers(text):
97
- """Remove line numbers from the beginning of each line."""
98
- lines = text.splitlines()
99
- result = []
100
- pattern = r'^(\d+[\.\)\:]?\s*|\[\d+\]\s*)'
101
-
102
- for line in lines:
103
- result.append(re.sub(pattern, '', line))
104
-
105
- return '\n'.join(result)
106
-
107
- @staticmethod
108
- def reverse_lines(text):
109
- """Reverse the order of lines."""
110
- lines = text.splitlines()
111
- return '\n'.join(reversed(lines))
112
-
113
- @staticmethod
114
- def shuffle_lines(text):
115
- """Randomly shuffle the order of lines."""
116
- lines = text.splitlines()
117
- random.shuffle(lines)
118
- return '\n'.join(lines)
119
-
120
- @staticmethod
121
- def process_text(input_text, tool_type, settings):
122
- """Process text using the specified line tool and settings."""
123
- if tool_type == "Remove Duplicates":
124
- return LineToolsProcessor.remove_duplicates(
125
- input_text,
126
- settings.get("duplicate_mode", "keep_first"),
127
- settings.get("case_sensitive", True)
128
- )
129
- elif tool_type == "Remove Empty Lines":
130
- return LineToolsProcessor.remove_empty_lines(
131
- input_text,
132
- settings.get("preserve_single", False)
133
- )
134
- elif tool_type == "Add Line Numbers":
135
- return LineToolsProcessor.add_line_numbers(
136
- input_text,
137
- settings.get("number_format", "1. "),
138
- settings.get("start_number", 1),
139
- settings.get("skip_empty", False)
140
- )
141
- elif tool_type == "Remove Line Numbers":
142
- return LineToolsProcessor.remove_line_numbers(input_text)
143
- elif tool_type == "Reverse Lines":
144
- return LineToolsProcessor.reverse_lines(input_text)
145
- elif tool_type == "Shuffle Lines":
146
- return LineToolsProcessor.shuffle_lines(input_text)
147
- else:
148
- return f"Unknown line tool: {tool_type}"
149
-
150
-
151
- class LineToolsWidget(ttk.Frame):
152
- """Tabbed interface widget for line tools."""
153
-
154
- def __init__(self, parent, app):
155
- super().__init__(parent)
156
- self.app = app
157
- self.processor = LineToolsProcessor()
158
-
159
- self.duplicate_mode = tk.StringVar(value="keep_first")
160
- self.case_sensitive = tk.BooleanVar(value=True)
161
- self.preserve_single = tk.BooleanVar(value=False)
162
- self.number_format = tk.StringVar(value="1. ")
163
- self.start_number = tk.IntVar(value=1)
164
- self.skip_empty = tk.BooleanVar(value=False)
165
-
166
- self.create_widgets()
167
- self.load_settings()
168
-
169
- def create_widgets(self):
170
- """Creates the tabbed interface for line tools."""
171
- self.notebook = ttk.Notebook(self)
172
- self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
173
-
174
- self.create_remove_duplicates_tab()
175
- self.create_remove_empty_tab()
176
- self.create_add_numbers_tab()
177
- self.create_remove_numbers_tab()
178
- self.create_reverse_tab()
179
- self.create_shuffle_tab()
180
-
181
- def create_remove_duplicates_tab(self):
182
- """Creates the Remove Duplicates tab."""
183
- frame = ttk.Frame(self.notebook)
184
- self.notebook.add(frame, text="Remove Duplicates")
185
-
186
- mode_frame = ttk.LabelFrame(frame, text="Duplicate Handling", padding=10)
187
- mode_frame.pack(fill=tk.X, padx=5, pady=5)
188
-
189
- ttk.Radiobutton(mode_frame, text="Keep First Occurrence",
190
- variable=self.duplicate_mode, value="keep_first",
191
- command=self.on_setting_change).pack(anchor=tk.W)
192
- ttk.Radiobutton(mode_frame, text="Keep Last Occurrence",
193
- variable=self.duplicate_mode, value="keep_last",
194
- command=self.on_setting_change).pack(anchor=tk.W)
195
-
196
- ttk.Checkbutton(frame, text="Case Sensitive",
197
- variable=self.case_sensitive,
198
- command=self.on_setting_change).pack(anchor=tk.W, padx=5, pady=5)
199
-
200
- ttk.Button(frame, text="Remove Duplicates",
201
- command=lambda: self.process("Remove Duplicates")).pack(pady=10)
202
-
203
- def create_remove_empty_tab(self):
204
- """Creates the Remove Empty Lines tab."""
205
- frame = ttk.Frame(self.notebook)
206
- self.notebook.add(frame, text="Remove Empty")
207
-
208
- options_frame = ttk.LabelFrame(frame, text="Options", padding=10)
209
- options_frame.pack(fill=tk.X, padx=5, pady=5)
210
-
211
- ttk.Checkbutton(options_frame, text="Preserve Single Empty Lines (collapse multiple)",
212
- variable=self.preserve_single,
213
- command=self.on_setting_change).pack(anchor=tk.W)
214
-
215
- ttk.Button(frame, text="Remove Empty Lines",
216
- command=lambda: self.process("Remove Empty Lines")).pack(pady=10)
217
-
218
- def create_add_numbers_tab(self):
219
- """Creates the Add Line Numbers tab."""
220
- frame = ttk.Frame(self.notebook)
221
- self.notebook.add(frame, text="Add Numbers")
222
-
223
- format_frame = ttk.LabelFrame(frame, text="Number Format", padding=10)
224
- format_frame.pack(fill=tk.X, padx=5, pady=5)
225
-
226
- formats = [("1. (dot)", "1. "), ("1) (parenthesis)", "1) "),
227
- ("[1] (brackets)", "[1] "), ("1: (colon)", "1: ")]
228
- for text, value in formats:
229
- ttk.Radiobutton(format_frame, text=text,
230
- variable=self.number_format, value=value,
231
- command=self.on_setting_change).pack(anchor=tk.W)
232
-
233
- start_frame = ttk.Frame(frame)
234
- start_frame.pack(fill=tk.X, padx=5, pady=5)
235
- ttk.Label(start_frame, text="Start Number:").pack(side=tk.LEFT)
236
- ttk.Spinbox(start_frame, from_=0, to=9999, width=6,
237
- textvariable=self.start_number,
238
- command=self.on_setting_change).pack(side=tk.LEFT, padx=5)
239
-
240
- ttk.Checkbutton(frame, text="Skip Empty Lines",
241
- variable=self.skip_empty,
242
- command=self.on_setting_change).pack(anchor=tk.W, padx=5)
243
-
244
- ttk.Button(frame, text="Add Line Numbers",
245
- command=lambda: self.process("Add Line Numbers")).pack(pady=10)
246
-
247
- def create_remove_numbers_tab(self):
248
- """Creates the Remove Line Numbers tab."""
249
- frame = ttk.Frame(self.notebook)
250
- self.notebook.add(frame, text="Remove Numbers")
251
-
252
- info = ttk.Label(frame, text="Removes line numbers from the beginning of each line.\n"
253
- "Supports formats: 1. , 1) , [1] , 1: , 1 ",
254
- justify=tk.CENTER)
255
- info.pack(pady=20)
256
-
257
- ttk.Button(frame, text="Remove Line Numbers",
258
- command=lambda: self.process("Remove Line Numbers")).pack(pady=10)
259
-
260
- def create_reverse_tab(self):
261
- """Creates the Reverse Lines tab."""
262
- frame = ttk.Frame(self.notebook)
263
- self.notebook.add(frame, text="Reverse")
264
-
265
- info = ttk.Label(frame, text="Reverses the order of all lines in the text.",
266
- justify=tk.CENTER)
267
- info.pack(pady=20)
268
-
269
- ttk.Button(frame, text="Reverse Lines",
270
- command=lambda: self.process("Reverse Lines")).pack(pady=10)
271
-
272
- def create_shuffle_tab(self):
273
- """Creates the Shuffle Lines tab."""
274
- frame = ttk.Frame(self.notebook)
275
- self.notebook.add(frame, text="Shuffle")
276
-
277
- info = ttk.Label(frame, text="Randomly shuffles the order of all lines.",
278
- justify=tk.CENTER)
279
- info.pack(pady=20)
280
-
281
- ttk.Button(frame, text="Shuffle Lines",
282
- command=lambda: self.process("Shuffle Lines")).pack(pady=10)
283
-
284
- def load_settings(self):
285
- """Load settings from the application."""
286
- settings = self.app.settings.get("tool_settings", {}).get("Line Tools", {})
287
-
288
- self.duplicate_mode.set(settings.get("duplicate_mode", "keep_first"))
289
- self.case_sensitive.set(settings.get("case_sensitive", True))
290
- self.preserve_single.set(settings.get("preserve_single", False))
291
- self.number_format.set(settings.get("number_format", "1. "))
292
- self.start_number.set(settings.get("start_number", 1))
293
- self.skip_empty.set(settings.get("skip_empty", False))
294
-
295
- def save_settings(self):
296
- """Save current settings to the application."""
297
- if "Line Tools" not in self.app.settings["tool_settings"]:
298
- self.app.settings["tool_settings"]["Line Tools"] = {}
299
-
300
- self.app.settings["tool_settings"]["Line Tools"].update({
301
- "duplicate_mode": self.duplicate_mode.get(),
302
- "case_sensitive": self.case_sensitive.get(),
303
- "preserve_single": self.preserve_single.get(),
304
- "number_format": self.number_format.get(),
305
- "start_number": self.start_number.get(),
306
- "skip_empty": self.skip_empty.get()
307
- })
308
-
309
- self.app.save_settings()
310
-
311
- def on_setting_change(self, *args):
312
- """Handle setting changes."""
313
- self.save_settings()
314
-
315
- def process(self, tool_type):
316
- """Process the input text with the selected tool."""
317
- active_input_tab = self.app.input_tabs[self.app.input_notebook.index(self.app.input_notebook.select())]
318
- input_text = active_input_tab.text.get("1.0", tk.END).rstrip('\n')
319
-
320
- if not input_text.strip():
321
- return
322
-
323
- settings = {
324
- "duplicate_mode": self.duplicate_mode.get(),
325
- "case_sensitive": self.case_sensitive.get(),
326
- "preserve_single": self.preserve_single.get(),
327
- "number_format": self.number_format.get(),
328
- "start_number": self.start_number.get(),
329
- "skip_empty": self.skip_empty.get()
330
- }
331
-
332
- result = LineToolsProcessor.process_text(input_text, tool_type, settings)
333
-
334
- active_output_tab = self.app.output_tabs[self.app.output_notebook.index(self.app.output_notebook.select())]
335
- active_output_tab.text.config(state="normal")
336
- active_output_tab.text.delete("1.0", tk.END)
337
- active_output_tab.text.insert("1.0", result)
338
- active_output_tab.text.config(state="disabled")
339
-
340
- self.app.update_all_stats()
341
-
342
-
343
- class LineTools:
344
- """Main class for Line Tools integration."""
345
-
346
- def __init__(self):
347
- self.processor = LineToolsProcessor()
348
-
349
- def create_widget(self, parent, app):
350
- """Create and return the Line Tools widget."""
351
- return LineToolsWidget(parent, app)
352
-
353
- def get_default_settings(self):
354
- """Return default settings for Line Tools."""
355
- return {
356
- "duplicate_mode": "keep_first",
357
- "case_sensitive": True,
358
- "preserve_single": False,
359
- "number_format": "1. ",
360
- "start_number": 1,
361
- "skip_empty": False
362
- }
363
-
364
- def process_text(self, input_text, tool_type, settings):
365
- """Process text using the specified tool and settings."""
366
- return LineToolsProcessor.process_text(input_text, tool_type, settings)
367
-
368
-
369
- # BaseTool-compatible wrapper
370
- try:
371
- from tools.base_tool import ToolWithOptions
372
- from typing import Dict, Any
373
-
374
- class LineToolsV2(ToolWithOptions):
375
- """
376
- BaseTool-compatible version of LineTools.
377
- """
378
-
379
- TOOL_NAME = "Line Tools"
380
- TOOL_DESCRIPTION = "Line manipulation: remove duplicates, add numbers, reverse, shuffle"
381
- TOOL_VERSION = "2.0.0"
382
-
383
- OPTIONS = [
384
- ("Remove Duplicates", "remove_duplicates"),
385
- ("Remove Empty Lines", "remove_empty"),
386
- ("Add Line Numbers", "add_numbers"),
387
- ("Remove Line Numbers", "remove_numbers"),
388
- ("Reverse Lines", "reverse"),
389
- ("Shuffle Lines", "shuffle"),
390
- ]
391
- OPTIONS_LABEL = "Operation"
392
- USE_DROPDOWN = True
393
- DEFAULT_OPTION = "remove_duplicates"
394
-
395
- def __init__(self):
396
- super().__init__()
397
- self._processor = LineToolsProcessor()
398
-
399
- def process_text(self, input_text: str, settings: Dict[str, Any]) -> str:
400
- """Process text using the specified line operation."""
401
- mode = settings.get("mode", "remove_duplicates")
402
-
403
- if mode == "remove_duplicates":
404
- return LineToolsProcessor.remove_duplicates(input_text)
405
- elif mode == "remove_empty":
406
- return LineToolsProcessor.remove_empty_lines(input_text)
407
- elif mode == "add_numbers":
408
- return LineToolsProcessor.add_line_numbers(input_text)
409
- elif mode == "remove_numbers":
410
- return LineToolsProcessor.remove_line_numbers(input_text)
411
- elif mode == "reverse":
412
- return LineToolsProcessor.reverse_lines(input_text)
413
- elif mode == "shuffle":
414
- return LineToolsProcessor.shuffle_lines(input_text)
415
- else:
416
- return input_text
417
-
418
- except ImportError:
419
- pass
1
+ """
2
+ Line Tools Module - Line manipulation utilities
3
+
4
+ This module provides comprehensive line manipulation functionality with a tabbed UI interface
5
+ for the Pomera AI Commander application.
6
+
7
+ Features:
8
+ - Remove Duplicates: Remove duplicate lines (keep first/last, case-sensitive option)
9
+ - Remove Empty Lines: Remove blank/whitespace-only lines
10
+ - Add Line Numbers: Prefix lines with numbers
11
+ - Remove Line Numbers: Strip leading numbers from lines
12
+ - Reverse Lines: Reverse line order
13
+ - Shuffle Lines: Randomize line order
14
+ """
15
+
16
+ import tkinter as tk
17
+ from tkinter import ttk
18
+ import random
19
+ import re
20
+
21
+
22
+ class LineToolsProcessor:
23
+ """Line tools processor with various line manipulation capabilities."""
24
+
25
+ @staticmethod
26
+ def remove_duplicates(text, mode="keep_first", case_sensitive=True):
27
+ """Remove duplicate lines from text."""
28
+ lines = text.splitlines()
29
+
30
+ if mode == "keep_first":
31
+ seen = set()
32
+ result = []
33
+ for line in lines:
34
+ key = line if case_sensitive else line.lower()
35
+ if key not in seen:
36
+ seen.add(key)
37
+ result.append(line)
38
+ else: # keep_last
39
+ seen = {}
40
+ for i, line in enumerate(lines):
41
+ key = line if case_sensitive else line.lower()
42
+ seen[key] = (i, line)
43
+ result = [v[1] for v in sorted(seen.values(), key=lambda x: x[0])]
44
+
45
+ return '\n'.join(result)
46
+
47
+ @staticmethod
48
+ def remove_empty_lines(text, preserve_single=False):
49
+ """Remove empty or whitespace-only lines."""
50
+ lines = text.splitlines()
51
+
52
+ if preserve_single:
53
+ result = []
54
+ prev_empty = False
55
+ for line in lines:
56
+ is_empty = not line.strip()
57
+ if is_empty:
58
+ if not prev_empty:
59
+ result.append('')
60
+ prev_empty = True
61
+ else:
62
+ result.append(line)
63
+ prev_empty = False
64
+ else:
65
+ result = [line for line in lines if line.strip()]
66
+
67
+ return '\n'.join(result)
68
+
69
+ @staticmethod
70
+ def add_line_numbers(text, format_style="1. ", start_number=1, skip_empty=False):
71
+ """Add line numbers to each line."""
72
+ lines = text.splitlines()
73
+ result = []
74
+ num = start_number
75
+
76
+ for line in lines:
77
+ if skip_empty and not line.strip():
78
+ result.append(line)
79
+ else:
80
+ if format_style == "1. ":
81
+ prefix = f"{num}. "
82
+ elif format_style == "1) ":
83
+ prefix = f"{num}) "
84
+ elif format_style == "[1] ":
85
+ prefix = f"[{num}] "
86
+ elif format_style == "1: ":
87
+ prefix = f"{num}: "
88
+ else:
89
+ prefix = f"{num}. "
90
+ result.append(f"{prefix}{line}")
91
+ num += 1
92
+
93
+ return '\n'.join(result)
94
+
95
+ @staticmethod
96
+ def remove_line_numbers(text):
97
+ """Remove line numbers from the beginning of each line."""
98
+ lines = text.splitlines()
99
+ result = []
100
+ pattern = r'^(\d+[\.\)\:]?\s*|\[\d+\]\s*)'
101
+
102
+ for line in lines:
103
+ result.append(re.sub(pattern, '', line))
104
+
105
+ return '\n'.join(result)
106
+
107
+ @staticmethod
108
+ def reverse_lines(text):
109
+ """Reverse the order of lines."""
110
+ lines = text.splitlines()
111
+ return '\n'.join(reversed(lines))
112
+
113
+ @staticmethod
114
+ def shuffle_lines(text):
115
+ """Randomly shuffle the order of lines."""
116
+ lines = text.splitlines()
117
+ random.shuffle(lines)
118
+ return '\n'.join(lines)
119
+
120
+ @staticmethod
121
+ def process_text(input_text, tool_type, settings):
122
+ """Process text using the specified line tool and settings."""
123
+ if tool_type == "Remove Duplicates":
124
+ return LineToolsProcessor.remove_duplicates(
125
+ input_text,
126
+ settings.get("duplicate_mode", "keep_first"),
127
+ settings.get("case_sensitive", True)
128
+ )
129
+ elif tool_type == "Remove Empty Lines":
130
+ return LineToolsProcessor.remove_empty_lines(
131
+ input_text,
132
+ settings.get("preserve_single", False)
133
+ )
134
+ elif tool_type == "Add Line Numbers":
135
+ return LineToolsProcessor.add_line_numbers(
136
+ input_text,
137
+ settings.get("number_format", "1. "),
138
+ settings.get("start_number", 1),
139
+ settings.get("skip_empty", False)
140
+ )
141
+ elif tool_type == "Remove Line Numbers":
142
+ return LineToolsProcessor.remove_line_numbers(input_text)
143
+ elif tool_type == "Reverse Lines":
144
+ return LineToolsProcessor.reverse_lines(input_text)
145
+ elif tool_type == "Shuffle Lines":
146
+ return LineToolsProcessor.shuffle_lines(input_text)
147
+ else:
148
+ return f"Unknown line tool: {tool_type}"
149
+
150
+
151
+ class LineToolsWidget(ttk.Frame):
152
+ """Tabbed interface widget for line tools."""
153
+
154
+ def __init__(self, parent, app):
155
+ super().__init__(parent)
156
+ self.app = app
157
+ self.processor = LineToolsProcessor()
158
+
159
+ self.duplicate_mode = tk.StringVar(value="keep_first")
160
+ self.case_sensitive = tk.BooleanVar(value=True)
161
+ self.preserve_single = tk.BooleanVar(value=False)
162
+ self.number_format = tk.StringVar(value="1. ")
163
+ self.start_number = tk.IntVar(value=1)
164
+ self.skip_empty = tk.BooleanVar(value=False)
165
+
166
+ self.create_widgets()
167
+ self.load_settings()
168
+
169
+ def create_widgets(self):
170
+ """Creates the tabbed interface for line tools."""
171
+ self.notebook = ttk.Notebook(self)
172
+ self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
173
+
174
+ self.create_remove_duplicates_tab()
175
+ self.create_remove_empty_tab()
176
+ self.create_add_numbers_tab()
177
+ self.create_remove_numbers_tab()
178
+ self.create_reverse_tab()
179
+ self.create_shuffle_tab()
180
+
181
+ def create_remove_duplicates_tab(self):
182
+ """Creates the Remove Duplicates tab."""
183
+ frame = ttk.Frame(self.notebook)
184
+ self.notebook.add(frame, text="Remove Duplicates")
185
+
186
+ mode_frame = ttk.LabelFrame(frame, text="Duplicate Handling", padding=10)
187
+ mode_frame.pack(fill=tk.X, padx=5, pady=5)
188
+
189
+ ttk.Radiobutton(mode_frame, text="Keep First Occurrence",
190
+ variable=self.duplicate_mode, value="keep_first",
191
+ command=self.on_setting_change).pack(anchor=tk.W)
192
+ ttk.Radiobutton(mode_frame, text="Keep Last Occurrence",
193
+ variable=self.duplicate_mode, value="keep_last",
194
+ command=self.on_setting_change).pack(anchor=tk.W)
195
+
196
+ ttk.Checkbutton(frame, text="Case Sensitive",
197
+ variable=self.case_sensitive,
198
+ command=self.on_setting_change).pack(anchor=tk.W, padx=5, pady=5)
199
+
200
+ ttk.Button(frame, text="Remove Duplicates",
201
+ command=lambda: self.process("Remove Duplicates")).pack(pady=10)
202
+
203
+ def create_remove_empty_tab(self):
204
+ """Creates the Remove Empty Lines tab."""
205
+ frame = ttk.Frame(self.notebook)
206
+ self.notebook.add(frame, text="Remove Empty")
207
+
208
+ options_frame = ttk.LabelFrame(frame, text="Options", padding=10)
209
+ options_frame.pack(fill=tk.X, padx=5, pady=5)
210
+
211
+ ttk.Checkbutton(options_frame, text="Preserve Single Empty Lines (collapse multiple)",
212
+ variable=self.preserve_single,
213
+ command=self.on_setting_change).pack(anchor=tk.W)
214
+
215
+ ttk.Button(frame, text="Remove Empty Lines",
216
+ command=lambda: self.process("Remove Empty Lines")).pack(pady=10)
217
+
218
+ def create_add_numbers_tab(self):
219
+ """Creates the Add Line Numbers tab."""
220
+ frame = ttk.Frame(self.notebook)
221
+ self.notebook.add(frame, text="Add Numbers")
222
+
223
+ format_frame = ttk.LabelFrame(frame, text="Number Format", padding=10)
224
+ format_frame.pack(fill=tk.X, padx=5, pady=5)
225
+
226
+ formats = [("1. (dot)", "1. "), ("1) (parenthesis)", "1) "),
227
+ ("[1] (brackets)", "[1] "), ("1: (colon)", "1: ")]
228
+ for text, value in formats:
229
+ ttk.Radiobutton(format_frame, text=text,
230
+ variable=self.number_format, value=value,
231
+ command=self.on_setting_change).pack(anchor=tk.W)
232
+
233
+ start_frame = ttk.Frame(frame)
234
+ start_frame.pack(fill=tk.X, padx=5, pady=5)
235
+ ttk.Label(start_frame, text="Start Number:").pack(side=tk.LEFT)
236
+ ttk.Spinbox(start_frame, from_=0, to=9999, width=6,
237
+ textvariable=self.start_number,
238
+ command=self.on_setting_change).pack(side=tk.LEFT, padx=5)
239
+
240
+ ttk.Checkbutton(frame, text="Skip Empty Lines",
241
+ variable=self.skip_empty,
242
+ command=self.on_setting_change).pack(anchor=tk.W, padx=5)
243
+
244
+ ttk.Button(frame, text="Add Line Numbers",
245
+ command=lambda: self.process("Add Line Numbers")).pack(pady=10)
246
+
247
+ def create_remove_numbers_tab(self):
248
+ """Creates the Remove Line Numbers tab."""
249
+ frame = ttk.Frame(self.notebook)
250
+ self.notebook.add(frame, text="Remove Numbers")
251
+
252
+ info = ttk.Label(frame, text="Removes line numbers from the beginning of each line.\n"
253
+ "Supports formats: 1. , 1) , [1] , 1: , 1 ",
254
+ justify=tk.CENTER)
255
+ info.pack(pady=20)
256
+
257
+ ttk.Button(frame, text="Remove Line Numbers",
258
+ command=lambda: self.process("Remove Line Numbers")).pack(pady=10)
259
+
260
+ def create_reverse_tab(self):
261
+ """Creates the Reverse Lines tab."""
262
+ frame = ttk.Frame(self.notebook)
263
+ self.notebook.add(frame, text="Reverse")
264
+
265
+ info = ttk.Label(frame, text="Reverses the order of all lines in the text.",
266
+ justify=tk.CENTER)
267
+ info.pack(pady=20)
268
+
269
+ ttk.Button(frame, text="Reverse Lines",
270
+ command=lambda: self.process("Reverse Lines")).pack(pady=10)
271
+
272
+ def create_shuffle_tab(self):
273
+ """Creates the Shuffle Lines tab."""
274
+ frame = ttk.Frame(self.notebook)
275
+ self.notebook.add(frame, text="Shuffle")
276
+
277
+ info = ttk.Label(frame, text="Randomly shuffles the order of all lines.",
278
+ justify=tk.CENTER)
279
+ info.pack(pady=20)
280
+
281
+ ttk.Button(frame, text="Shuffle Lines",
282
+ command=lambda: self.process("Shuffle Lines")).pack(pady=10)
283
+
284
+ def load_settings(self):
285
+ """Load settings from the application."""
286
+ settings = self.app.settings.get("tool_settings", {}).get("Line Tools", {})
287
+
288
+ self.duplicate_mode.set(settings.get("duplicate_mode", "keep_first"))
289
+ self.case_sensitive.set(settings.get("case_sensitive", True))
290
+ self.preserve_single.set(settings.get("preserve_single", False))
291
+ self.number_format.set(settings.get("number_format", "1. "))
292
+ self.start_number.set(settings.get("start_number", 1))
293
+ self.skip_empty.set(settings.get("skip_empty", False))
294
+
295
+ def save_settings(self):
296
+ """Save current settings to the application."""
297
+ if "Line Tools" not in self.app.settings["tool_settings"]:
298
+ self.app.settings["tool_settings"]["Line Tools"] = {}
299
+
300
+ self.app.settings["tool_settings"]["Line Tools"].update({
301
+ "duplicate_mode": self.duplicate_mode.get(),
302
+ "case_sensitive": self.case_sensitive.get(),
303
+ "preserve_single": self.preserve_single.get(),
304
+ "number_format": self.number_format.get(),
305
+ "start_number": self.start_number.get(),
306
+ "skip_empty": self.skip_empty.get()
307
+ })
308
+
309
+ self.app.save_settings()
310
+
311
+ def on_setting_change(self, *args):
312
+ """Handle setting changes."""
313
+ self.save_settings()
314
+
315
+ def process(self, tool_type):
316
+ """Process the input text with the selected tool."""
317
+ active_input_tab = self.app.input_tabs[self.app.input_notebook.index(self.app.input_notebook.select())]
318
+ input_text = active_input_tab.text.get("1.0", tk.END).rstrip('\n')
319
+
320
+ if not input_text.strip():
321
+ return
322
+
323
+ settings = {
324
+ "duplicate_mode": self.duplicate_mode.get(),
325
+ "case_sensitive": self.case_sensitive.get(),
326
+ "preserve_single": self.preserve_single.get(),
327
+ "number_format": self.number_format.get(),
328
+ "start_number": self.start_number.get(),
329
+ "skip_empty": self.skip_empty.get()
330
+ }
331
+
332
+ result = LineToolsProcessor.process_text(input_text, tool_type, settings)
333
+
334
+ active_output_tab = self.app.output_tabs[self.app.output_notebook.index(self.app.output_notebook.select())]
335
+ active_output_tab.text.config(state="normal")
336
+ active_output_tab.text.delete("1.0", tk.END)
337
+ active_output_tab.text.insert("1.0", result)
338
+ active_output_tab.text.config(state="disabled")
339
+
340
+ self.app.update_all_stats()
341
+
342
+
343
+ class LineTools:
344
+ """Main class for Line Tools integration."""
345
+
346
+ def __init__(self):
347
+ self.processor = LineToolsProcessor()
348
+
349
+ def create_widget(self, parent, app):
350
+ """Create and return the Line Tools widget."""
351
+ return LineToolsWidget(parent, app)
352
+
353
+ def get_default_settings(self):
354
+ """Return default settings for Line Tools."""
355
+ return {
356
+ "duplicate_mode": "keep_first",
357
+ "case_sensitive": True,
358
+ "preserve_single": False,
359
+ "number_format": "1. ",
360
+ "start_number": 1,
361
+ "skip_empty": False
362
+ }
363
+
364
+ def process_text(self, input_text, tool_type, settings):
365
+ """Process text using the specified tool and settings."""
366
+ return LineToolsProcessor.process_text(input_text, tool_type, settings)
367
+
368
+
369
+ # BaseTool-compatible wrapper
370
+ try:
371
+ from tools.base_tool import ToolWithOptions
372
+ from typing import Dict, Any
373
+
374
+ class LineToolsV2(ToolWithOptions):
375
+ """
376
+ BaseTool-compatible version of LineTools.
377
+ """
378
+
379
+ TOOL_NAME = "Line Tools"
380
+ TOOL_DESCRIPTION = "Line manipulation: remove duplicates, add numbers, reverse, shuffle"
381
+ TOOL_VERSION = "2.0.0"
382
+
383
+ OPTIONS = [
384
+ ("Remove Duplicates", "remove_duplicates"),
385
+ ("Remove Empty Lines", "remove_empty"),
386
+ ("Add Line Numbers", "add_numbers"),
387
+ ("Remove Line Numbers", "remove_numbers"),
388
+ ("Reverse Lines", "reverse"),
389
+ ("Shuffle Lines", "shuffle"),
390
+ ]
391
+ OPTIONS_LABEL = "Operation"
392
+ USE_DROPDOWN = True
393
+ DEFAULT_OPTION = "remove_duplicates"
394
+
395
+ def __init__(self):
396
+ super().__init__()
397
+ self._processor = LineToolsProcessor()
398
+
399
+ def process_text(self, input_text: str, settings: Dict[str, Any]) -> str:
400
+ """Process text using the specified line operation."""
401
+ mode = settings.get("mode", "remove_duplicates")
402
+
403
+ if mode == "remove_duplicates":
404
+ return LineToolsProcessor.remove_duplicates(input_text)
405
+ elif mode == "remove_empty":
406
+ return LineToolsProcessor.remove_empty_lines(input_text)
407
+ elif mode == "add_numbers":
408
+ return LineToolsProcessor.add_line_numbers(input_text)
409
+ elif mode == "remove_numbers":
410
+ return LineToolsProcessor.remove_line_numbers(input_text)
411
+ elif mode == "reverse":
412
+ return LineToolsProcessor.reverse_lines(input_text)
413
+ elif mode == "shuffle":
414
+ return LineToolsProcessor.shuffle_lines(input_text)
415
+ else:
416
+ return input_text
417
+
418
+ except ImportError:
419
+ pass