tdsearch 0.1.1__tar.gz

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.
tdsearch-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (s) 2026 Ivan Cvitic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction handling, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdsearch
3
+ Version: 0.1.1
4
+ Summary: Desktop (GUI) for Tabular Data Search powered by xlsxgrep.
5
+ Home-page: https://github.com/zazuum/TDsearch
6
+ Author: Ivan Cvitic
7
+ Author-email: cviticivan@gmail.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/zazuum/TDsearch
10
+ Project-URL: Tracker, https://github.com/zazuum/TDsearch/issues
11
+ Classifier: Environment :: X11 Applications :: Qt
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Environment :: Win32 (MS Windows)
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Office/Business
19
+ Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
20
+ Classifier: Topic :: Utilities
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: xlsxgrep==0.0.34
24
+ Requires-Dist: PySide6>=6.0.0
25
+ Dynamic: license-file
26
+
27
+ # TDsearch (Tabular Data Search)
28
+
29
+ **TDsearch** is a desktop GUI application designed to search text across spreadsheet files including XLSX, XLS, XLSM, CSV, TSV, and ODS formats.
30
+
31
+ ## Features
32
+ - **GUI Interface:** Modern PySide6 interface with file and folder browsers.
33
+ - **Multiprocessing Support:** Multi-core parallel search execution.
34
+ - **Regex Support:** Python regex and POSIX Extended Regular Expressions (-E).
35
+ - **Search Options:** Ignore case (-i), fixed strings (-F), word match (-w), column search mode.
36
+ - **CSV Export:** Export structured search results directly to CSV.
37
+
38
+ ## Installation
39
+ ```sh
40
+ pip install tdsearch
41
+ ```
42
+
43
+ ## Launch
44
+ ```sh
45
+ tdsearch
46
+ ```
@@ -0,0 +1,20 @@
1
+ # TDsearch (Tabular Data Search)
2
+
3
+ **TDsearch** is a desktop GUI application designed to search text across spreadsheet files including XLSX, XLS, XLSM, CSV, TSV, and ODS formats.
4
+
5
+ ## Features
6
+ - **GUI Interface:** Modern PySide6 interface with file and folder browsers.
7
+ - **Multiprocessing Support:** Multi-core parallel search execution.
8
+ - **Regex Support:** Python regex and POSIX Extended Regular Expressions (-E).
9
+ - **Search Options:** Ignore case (-i), fixed strings (-F), word match (-w), column search mode.
10
+ - **CSV Export:** Export structured search results directly to CSV.
11
+
12
+ ## Installation
13
+ ```sh
14
+ pip install tdsearch
15
+ ```
16
+
17
+ ## Launch
18
+ ```sh
19
+ tdsearch
20
+ ```
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,41 @@
1
+ [metadata]
2
+ name = tdsearch
3
+ version = attr: tdsearch.__version__
4
+ author = Ivan Cvitic
5
+ author_email = cviticivan@gmail.com
6
+ license = MIT
7
+ license_files = LICENSE
8
+ description = Desktop (GUI) for Tabular Data Search powered by xlsxgrep.
9
+ url = https://github.com/zazuum/TDsearch
10
+ project_urls =
11
+ Source = https://github.com/zazuum/TDsearch
12
+ Tracker = https://github.com/zazuum/TDsearch/issues
13
+ long_description = file: README.md
14
+ long_description_content_type = text/markdown
15
+ classifiers =
16
+ Environment :: X11 Applications :: Qt
17
+ Environment :: MacOS X
18
+ Environment :: Win32 (MS Windows)
19
+ License :: OSI Approved :: MIT License
20
+ Intended Audience :: End Users/Desktop
21
+ Operating System :: OS Independent
22
+ Programming Language :: Python :: 3
23
+ Topic :: Office/Business
24
+ Topic :: Office/Business :: Financial :: Spreadsheet
25
+ Topic :: Utilities
26
+
27
+ [options]
28
+ packages = tdsearch
29
+ install_requires =
30
+ xlsxgrep==0.0.34
31
+ PySide6>=6.0.0
32
+ include_package_data = True
33
+
34
+ [options.entry_points]
35
+ gui_scripts =
36
+ tdsearch = tdsearch.app:main
37
+
38
+ [egg_info]
39
+ tag_build =
40
+ tag_date = 0
41
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env python3
2
+ from setuptools import setup
3
+
4
+ if __name__ == "__main__":
5
+ setup()
@@ -0,0 +1,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ """TDsearch - Tabular Data Search Desktop GUI for xlsxgrep."""
3
+
4
+ __version__ = "0.1.1"
@@ -0,0 +1,1004 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """PySide6 Desktop Graphical User Interface (GUI) for Tabular Data Search (TDsearch)."""
5
+
6
+ import csv
7
+ import os
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from time import perf_counter
12
+
13
+ try:
14
+ from PySide6.QtCore import QSettings, QStandardPaths, QThread, Signal, Qt
15
+ from PySide6.QtWidgets import (
16
+ QApplication,
17
+ QMainWindow,
18
+ QWidget,
19
+ QVBoxLayout,
20
+ QHBoxLayout,
21
+ QGridLayout,
22
+ QLabel,
23
+ QLineEdit,
24
+ QPushButton,
25
+ QMenu,
26
+ QCheckBox,
27
+ QComboBox,
28
+ QTableWidget,
29
+ QTableWidgetItem,
30
+ QFileDialog,
31
+ QMessageBox,
32
+ QStatusBar,
33
+ QGroupBox,
34
+ QHeaderView,
35
+ QProgressBar,
36
+ QDialog,
37
+ QDialogButtonBox,
38
+ QPlainTextEdit,
39
+ QTabWidget,
40
+ QSizePolicy,
41
+ )
42
+ from PySide6.QtGui import QColor, QFont, QPalette
43
+ PYSIDE_AVAILABLE = True
44
+ except ImportError:
45
+ PYSIDE_AVAILABLE = False
46
+
47
+ try:
48
+ from xlsxgrep import process_single_file, __version__ as xlsxgrep_version
49
+ except ImportError:
50
+ try:
51
+ from xlsxgrep.xlsxgrep import process_single_file, __version__ as xlsxgrep_version
52
+ except ImportError:
53
+ process_single_file = None
54
+ xlsxgrep_version = "unknown"
55
+
56
+ from tdsearch import __version__ as tdsearch_version
57
+
58
+
59
+ SUPPORTED_EXTENSIONS = (
60
+ ".xls",
61
+ ".xlsx",
62
+ ".ods",
63
+ ".csv",
64
+ ".tsv",
65
+ ".xlsm",
66
+ )
67
+
68
+
69
+ IGNORE_CASE_INLINE_FLAG_RE = re.compile(r"^\(\?[a-zA-Z-]*i[a-zA-Z-]*\)")
70
+ SETTINGS_ORGANIZATION = "TDsearch"
71
+ SETTINGS_APPLICATION = "TDsearch"
72
+ VALID_THEME_CHOICES = {"system", "dark", "light"}
73
+
74
+
75
+ def collect_target_files(path_str, recursive=True):
76
+ path = Path(path_str)
77
+ file_list = []
78
+ if not path.exists():
79
+ return file_list
80
+
81
+ if path.is_file():
82
+ if path.suffix.lower() in SUPPORTED_EXTENSIONS:
83
+ file_list.append(str(path))
84
+ elif path.is_dir():
85
+ pattern = "**/*" if recursive else "*"
86
+ for p in path.glob(pattern):
87
+ if p.is_file() and p.suffix.lower() in SUPPORTED_EXTENSIONS:
88
+ file_list.append(str(p))
89
+
90
+ return file_list
91
+
92
+
93
+ def build_search_options(
94
+ pattern,
95
+ *,
96
+ fixed_strings,
97
+ regex_enabled,
98
+ posix_ere,
99
+ ignore_case,
100
+ word_regexp,
101
+ recursive,
102
+ jobs,
103
+ count,
104
+ with_filename,
105
+ with_sheetname,
106
+ files_with_match,
107
+ files_without_match,
108
+ column_mode,
109
+ separator,
110
+ debug=False,
111
+ ):
112
+ if regex_enabled:
113
+ fixed_strings = False
114
+ word_regexp = False
115
+ ignore_case = False
116
+ elif fixed_strings:
117
+ word_regexp = False
118
+
119
+ python_regex = regex_enabled and not posix_ere
120
+ extended_regexp = regex_enabled and posix_ere
121
+ effective_pattern = pattern
122
+ effective_ignore_case = ignore_case
123
+
124
+ if word_regexp:
125
+ escaped_pattern = re.escape(pattern)
126
+ effective_pattern = rf"(?<!\w){escaped_pattern}(?!\w)"
127
+ if ignore_case:
128
+ effective_pattern = f"(?i){effective_pattern}"
129
+ python_regex = True
130
+ extended_regexp = False
131
+ effective_ignore_case = False
132
+ word_regexp = False
133
+
134
+ elif python_regex and ignore_case and not IGNORE_CASE_INLINE_FLAG_RE.match(pattern):
135
+ effective_pattern = f"(?i){pattern}"
136
+ effective_ignore_case = False
137
+
138
+ return {
139
+ "PATTERN": effective_pattern,
140
+ "python_regex": python_regex,
141
+ "extended_regexp": extended_regexp,
142
+ "fixed_strings": fixed_strings,
143
+ "ignore_case": effective_ignore_case,
144
+ "word_regexp": word_regexp,
145
+ "recursive": recursive,
146
+ "jobs": jobs,
147
+ "count": count,
148
+ "with_filename": with_filename,
149
+ "with_sheetname": with_sheetname,
150
+ "files_with_match": files_with_match,
151
+ "files_without_match": files_without_match,
152
+ "column": column_mode,
153
+ "row": not column_mode,
154
+ "null": False,
155
+ "separator": separator,
156
+ "debug": debug,
157
+ }
158
+
159
+
160
+ def _pluralize(count, singular, plural=None):
161
+ if count == 1:
162
+ return singular
163
+ return plural or f"{singular}s"
164
+
165
+
166
+ def format_search_status_message(
167
+ opts,
168
+ *,
169
+ file_count,
170
+ result_count,
171
+ matched_group_count=0,
172
+ matched_file_count=0,
173
+ matched_cell_count=0,
174
+ matched_string_count=0,
175
+ ):
176
+ searched_file_label = _pluralize(file_count, "file")
177
+
178
+ if opts["files_with_match"]:
179
+ match_label = _pluralize(matched_file_count, "matching file")
180
+ return (
181
+ f"Search completed. Found {matched_file_count} {match_label} "
182
+ f"across {file_count} searched {searched_file_label}."
183
+ )
184
+
185
+ if opts["files_without_match"]:
186
+ match_label = _pluralize(matched_file_count, "file without a match", "files without a match")
187
+ return (
188
+ f"Search completed. Found {matched_file_count} {match_label} "
189
+ f"across {file_count} searched {searched_file_label}."
190
+ )
191
+
192
+ if opts["count"]:
193
+ group_label = _pluralize(
194
+ matched_group_count,
195
+ "matching column" if opts["column"] else "matching row",
196
+ )
197
+ cell_label = _pluralize(matched_cell_count, "cell")
198
+ string_label = _pluralize(matched_string_count, "string")
199
+ return (
200
+ f"Search completed. Found {matched_group_count} {group_label}, "
201
+ f"{matched_cell_count} {cell_label}, {matched_string_count} {string_label} "
202
+ f"across {file_count} {searched_file_label}."
203
+ )
204
+
205
+ if opts["column"]:
206
+ result_label = _pluralize(result_count, "result cell")
207
+ return (
208
+ f"Search completed. Displaying {result_count} {result_label} "
209
+ f"from matching columns across {file_count} {searched_file_label}."
210
+ )
211
+
212
+ result_label = _pluralize(result_count, "matching row")
213
+ return (
214
+ f"Search completed. Found {result_count} {result_label} "
215
+ f"across {file_count} {searched_file_label}."
216
+ )
217
+
218
+
219
+ def format_elapsed_time(seconds):
220
+ if seconds < 1:
221
+ return f"{seconds:.3f}s"
222
+ if seconds < 10:
223
+ return f"{seconds:.2f}s"
224
+ if seconds < 60:
225
+ return f"{seconds:.1f}s"
226
+
227
+ minutes = int(seconds // 60)
228
+ remaining_seconds = seconds - (minutes * 60)
229
+ if remaining_seconds.is_integer():
230
+ return f"{minutes}m {int(remaining_seconds)}s"
231
+ return f"{minutes}m {remaining_seconds:.1f}s"
232
+
233
+
234
+ def normalize_processing_errors(error_lines):
235
+ return [line.strip() for line in error_lines if line and line.strip()]
236
+
237
+
238
+ def format_processing_error_tooltip(error_count):
239
+ file_label = _pluralize(error_count, "file")
240
+ verb = "was" if error_count == 1 else "were"
241
+ return f"{error_count} {file_label} {verb} not processed. Click for details."
242
+
243
+
244
+ def format_processing_error_summary(error_count):
245
+ file_label = _pluralize(error_count, "file")
246
+ verb = "was" if error_count == 1 else "were"
247
+ return f"{error_count} {file_label} {verb} not processed."
248
+
249
+
250
+ def build_processing_error_details(error_lines):
251
+ return "\n".join(normalize_processing_errors(error_lines))
252
+
253
+
254
+ def build_progress_message(processed_files, total_files):
255
+ if total_files <= 0:
256
+ return "Searching across 0 files..."
257
+ return f"Searching... {processed_files}/{total_files} files processed"
258
+
259
+
260
+ def format_count_result_summary(opts, group_count, cell_count, string_count):
261
+ group_label = _pluralize(
262
+ group_count,
263
+ "matching column" if opts["column"] else "matching row",
264
+ )
265
+ cell_label = _pluralize(cell_count, "cell")
266
+ string_label = _pluralize(string_count, "string")
267
+ return (
268
+ f"{group_count} {group_label}, {cell_count} {cell_label}, "
269
+ f"{string_count} {string_label}"
270
+ )
271
+
272
+
273
+ def normalize_theme_choice(theme_choice):
274
+ if theme_choice in VALID_THEME_CHOICES:
275
+ return theme_choice
276
+ return "system"
277
+
278
+
279
+ def build_dark_palette():
280
+ palette = QPalette()
281
+ palette.setColor(QPalette.Window, QColor(53, 53, 53))
282
+ palette.setColor(QPalette.WindowText, Qt.white)
283
+ palette.setColor(QPalette.Base, QColor(35, 35, 35))
284
+ palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
285
+ palette.setColor(QPalette.ToolTipBase, Qt.white)
286
+ palette.setColor(QPalette.ToolTipText, Qt.white)
287
+ palette.setColor(QPalette.Text, Qt.white)
288
+ palette.setColor(QPalette.Button, QColor(53, 53, 53))
289
+ palette.setColor(QPalette.ButtonText, Qt.white)
290
+ palette.setColor(QPalette.BrightText, Qt.red)
291
+ palette.setColor(QPalette.Link, QColor(42, 130, 218))
292
+ palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
293
+ palette.setColor(QPalette.HighlightedText, Qt.black)
294
+ return palette
295
+
296
+
297
+ def build_light_palette():
298
+ palette = QPalette()
299
+ palette.setColor(QPalette.Window, QColor(245, 245, 245))
300
+ palette.setColor(QPalette.WindowText, Qt.black)
301
+ palette.setColor(QPalette.Base, Qt.white)
302
+ palette.setColor(QPalette.AlternateBase, QColor(238, 238, 238))
303
+ palette.setColor(QPalette.ToolTipBase, Qt.white)
304
+ palette.setColor(QPalette.ToolTipText, Qt.black)
305
+ palette.setColor(QPalette.Text, Qt.black)
306
+ palette.setColor(QPalette.Button, QColor(245, 245, 245))
307
+ palette.setColor(QPalette.ButtonText, Qt.black)
308
+ palette.setColor(QPalette.BrightText, Qt.red)
309
+ palette.setColor(QPalette.Link, QColor(0, 102, 204))
310
+ palette.setColor(QPalette.Highlight, QColor(76, 163, 224))
311
+ palette.setColor(QPalette.HighlightedText, Qt.white)
312
+ return palette
313
+
314
+
315
+ if PYSIDE_AVAILABLE:
316
+ def default_documents_directory():
317
+ documents_path = QStandardPaths.writableLocation(
318
+ QStandardPaths.StandardLocation.DocumentsLocation
319
+ )
320
+ if documents_path:
321
+ return documents_path
322
+ return str(Path.home())
323
+
324
+
325
+ class SearchWorkerThread(QThread):
326
+ """Background thread for executing searches without freezing the GUI."""
327
+
328
+ result_signal = Signal(dict)
329
+ progress_signal = Signal(int, int, str)
330
+
331
+ def __init__(self, path_str, recursive, opts):
332
+ super().__init__()
333
+ self.path_str = path_str
334
+ self.recursive = recursive
335
+ self.opts = opts
336
+
337
+ def run(self):
338
+ if process_single_file is None:
339
+ self.result_signal.emit({
340
+ "results": [],
341
+ "file_count": 0,
342
+ "result_count": 0,
343
+ "matched_group_count": 0,
344
+ "matched_cell_count": 0,
345
+ "matched_string_count": 0,
346
+ "matched_file_count": 0,
347
+ "error": "xlsxgrep engine is not installed. Please run: pip install xlsxgrep"
348
+ })
349
+ return
350
+
351
+ files = collect_target_files(self.path_str, self.recursive)
352
+ results = []
353
+ matched_group_count = 0
354
+ matched_cell_count = 0
355
+ matched_string_count = 0
356
+ matched_file_count = 0
357
+ processing_errors = []
358
+
359
+ total_files = len(files)
360
+ self.progress_signal.emit(0, max(total_files, 1), build_progress_message(0, total_files))
361
+
362
+ for idx, f in enumerate(files):
363
+ res = process_single_file(f, self.opts)
364
+ stdout_lines = res.get("stdout", [])
365
+ counts = res.get("counts", (0, 0, 0))
366
+ processing_errors.extend(normalize_processing_errors(res.get("stderr", [])))
367
+ matched_group_count += counts[0]
368
+ matched_cell_count += counts[1]
369
+ matched_string_count += counts[2]
370
+ if stdout_lines:
371
+ matched_file_count += 1
372
+
373
+ if self.opts["count"]:
374
+ if counts[0] > 0:
375
+ results.append((
376
+ f,
377
+ "",
378
+ format_count_result_summary(self.opts, counts[0], counts[1], counts[2]),
379
+ ))
380
+ else:
381
+ for line in stdout_lines:
382
+ line_str = line.rstrip("\r\n")
383
+ file_name = ""
384
+ sheet_name = ""
385
+ content = line_str
386
+ if self.opts["with_filename"] and self.opts["with_sheetname"]:
387
+ parts = line_str.split(": ", 2)
388
+ else:
389
+ parts = line_str.split(": ", 1)
390
+
391
+ if self.opts["with_filename"] and self.opts["with_sheetname"] and len(parts) == 3:
392
+ file_name, sheet_name, content = parts
393
+ elif self.opts["with_filename"] and len(parts) == 2:
394
+ file_name, content = parts
395
+ elif self.opts["with_sheetname"] and len(parts) == 2:
396
+ sheet_name, content = parts
397
+
398
+ results.append((file_name, sheet_name, content))
399
+
400
+ processed_files = idx + 1
401
+ self.progress_signal.emit(
402
+ processed_files,
403
+ max(total_files, 1),
404
+ build_progress_message(processed_files, total_files),
405
+ )
406
+
407
+ self.result_signal.emit({
408
+ "results": results,
409
+ "file_count": len(files),
410
+ "result_count": len(results),
411
+ "matched_group_count": matched_group_count,
412
+ "matched_cell_count": matched_cell_count,
413
+ "matched_string_count": matched_string_count,
414
+ "matched_file_count": matched_file_count,
415
+ "processing_errors": processing_errors,
416
+ "status_message": format_search_status_message(
417
+ self.opts,
418
+ file_count=len(files),
419
+ result_count=len(results),
420
+ matched_group_count=matched_group_count,
421
+ matched_file_count=matched_file_count,
422
+ matched_cell_count=matched_cell_count,
423
+ matched_string_count=matched_string_count,
424
+ ),
425
+ })
426
+
427
+ class TDSearchGUI(QMainWindow):
428
+ """TDsearch PySide6 Desktop Application."""
429
+
430
+ def __init__(self):
431
+ super().__init__()
432
+ self.setWindowTitle(f"TDsearch v{tdsearch_version} (xlsxgrep v{xlsxgrep_version})")
433
+ self.resize(900, 720)
434
+ self.search_start_time = None
435
+ self.processing_errors = []
436
+ self.settings = QSettings(SETTINGS_ORGANIZATION, SETTINGS_APPLICATION)
437
+ app = QApplication.instance()
438
+ self.original_palette = QPalette(app.palette())
439
+ self.original_style_name = app.style().objectName()
440
+ self.original_stylesheet = app.styleSheet()
441
+
442
+ self.init_ui()
443
+
444
+ def init_ui(self):
445
+ central_widget = QWidget()
446
+ self.setCentralWidget(central_widget)
447
+ main_layout = QVBoxLayout(central_widget)
448
+
449
+ # --- Search Controls Panel ---
450
+ panel = QGroupBox("Tabular Data Search Parameters")
451
+ panel_layout = QVBoxLayout(panel)
452
+
453
+ # Pattern row
454
+ row1 = QHBoxLayout()
455
+ row1.addWidget(QLabel("Pattern:"))
456
+ self.inp_pattern = QLineEdit()
457
+ self.inp_pattern.setPlaceholderText("Enter search string or regex pattern...")
458
+ self.inp_pattern.returnPressed.connect(self.start_search)
459
+ row1.addWidget(self.inp_pattern)
460
+ panel_layout.addLayout(row1)
461
+
462
+ # Path row
463
+ row2 = QHBoxLayout()
464
+ row2.addWidget(QLabel("Path:"))
465
+ self.inp_path = QLineEdit(default_documents_directory())
466
+ self.inp_path.setPlaceholderText("File or directory path...")
467
+ row2.addWidget(self.inp_path)
468
+
469
+ btn_open = QPushButton("Open...")
470
+ open_menu = QMenu(btn_open)
471
+ open_file_action = open_menu.addAction("Open File...")
472
+ open_file_action.triggered.connect(self.browse_file)
473
+ open_folder_action = open_menu.addAction("Open Folder...")
474
+ open_folder_action.triggered.connect(self.browse_directory)
475
+ btn_open.setMenu(open_menu)
476
+ row2.addWidget(btn_open)
477
+
478
+ panel_layout.addLayout(row2)
479
+
480
+ self.options_tabs = QTabWidget()
481
+ panel_layout.addWidget(self.options_tabs)
482
+
483
+ search_options_tab = QWidget()
484
+ search_options_layout = QVBoxLayout(search_options_tab)
485
+ search_options_layout.setContentsMargins(8, 8, 8, 8)
486
+
487
+ options_grid = QGridLayout()
488
+ options_grid.setHorizontalSpacing(18)
489
+ options_grid.setVerticalSpacing(8)
490
+ for column in range(4):
491
+ options_grid.setColumnStretch(column, 1)
492
+
493
+ self.chk_word_regexp = QCheckBox("Match whole words only")
494
+ self.chk_ignore_case = QCheckBox("Ignore uppercase/lowercase")
495
+ self.chk_ignore_case.setChecked(True)
496
+ self.chk_fixed_strings = QCheckBox("Plain text search")
497
+ self.chk_fixed_strings.setChecked(True)
498
+ self.combo_regex_type = QComboBox()
499
+ self.combo_regex_type.addItem("None", None)
500
+ self.combo_regex_type.addItem("Python regex", False)
501
+ self.combo_regex_type.addItem("POSIX regex", True)
502
+ self.chk_count = QCheckBox("Show counts only")
503
+
504
+ regex_options = QWidget()
505
+ regex_options_layout = QHBoxLayout(regex_options)
506
+ regex_options_layout.setContentsMargins(0, 0, 0, 0)
507
+ regex_options_layout.setSpacing(6)
508
+ regex_options_layout.addWidget(QLabel("Regex type:"))
509
+ self.combo_regex_type.setFixedWidth(120)
510
+ regex_options_layout.addWidget(self.combo_regex_type)
511
+ regex_options.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
512
+
513
+ options_grid.addWidget(self.chk_fixed_strings, 0, 0)
514
+ options_grid.addWidget(self.chk_word_regexp, 0, 1)
515
+ options_grid.addWidget(self.chk_ignore_case, 0, 2)
516
+
517
+ self.chk_recursive = QCheckBox("Search subfolders")
518
+ self.chk_recursive.setChecked(True)
519
+ self.chk_filename = QCheckBox("Show file name")
520
+ self.chk_sheetname = QCheckBox("Show sheet name")
521
+ self.chk_lmatch = QCheckBox("Show only matching files")
522
+ self.chk_lwithout = QCheckBox("Show only non-matching files")
523
+
524
+ options_grid.addWidget(self.chk_filename, 1, 0)
525
+ options_grid.addWidget(self.chk_sheetname, 1, 1)
526
+ options_grid.addWidget(self.chk_count, 1, 2)
527
+ options_grid.addWidget(self.chk_lmatch, 2, 0)
528
+ options_grid.addWidget(self.chk_lwithout, 2, 1)
529
+
530
+ self.chk_lmatch.toggled.connect(
531
+ lambda checked: self.chk_lwithout.setChecked(False) if checked else None
532
+ )
533
+ self.chk_lwithout.toggled.connect(
534
+ lambda checked: self.chk_lmatch.setChecked(False) if checked else None
535
+ )
536
+ self.chk_fixed_strings.toggled.connect(self.on_fixed_strings_toggled)
537
+ self.chk_word_regexp.toggled.connect(self.on_word_regexp_toggled)
538
+ self.combo_regex_type.currentIndexChanged.connect(self.on_regex_type_changed)
539
+ self.chk_ignore_case.toggled.connect(self.on_ignore_case_toggled)
540
+ self.chk_count.toggled.connect(self.on_count_toggled)
541
+ self.combo_search_mode = QComboBox()
542
+ self.combo_search_mode.addItem("Columns", True)
543
+ self.combo_search_mode.addItem("Rows", False)
544
+ self.combo_search_mode.setCurrentIndex(1)
545
+ self.chk_custom_separator = QCheckBox("Custom separator")
546
+ self.inp_sep = QLineEdit("\\t")
547
+ self.inp_sep.setFixedWidth(60)
548
+ self.inp_sep.setEnabled(False)
549
+ self.chk_custom_separator.toggled.connect(self.inp_sep.setEnabled)
550
+
551
+ search_mode_options = QWidget()
552
+ search_mode_layout = QHBoxLayout(search_mode_options)
553
+ search_mode_layout.setContentsMargins(0, 0, 0, 0)
554
+ search_mode_layout.addWidget(QLabel("Search by:"))
555
+ search_mode_layout.addWidget(self.combo_search_mode)
556
+
557
+ separator_options = QWidget()
558
+ separator_layout = QHBoxLayout(separator_options)
559
+ separator_layout.setContentsMargins(0, 0, 0, 0)
560
+ separator_layout.setSpacing(6)
561
+ separator_layout.addWidget(self.chk_custom_separator)
562
+ separator_layout.addWidget(self.inp_sep, 0, Qt.AlignLeft)
563
+ separator_options.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
564
+
565
+ right_options = QWidget()
566
+ right_options_layout = QVBoxLayout(right_options)
567
+ right_options_layout.setContentsMargins(0, 0, 0, 0)
568
+ right_options_layout.setSpacing(8)
569
+ right_options_layout.addWidget(regex_options)
570
+ right_options_layout.addWidget(search_mode_options)
571
+ right_options_layout.addWidget(separator_options)
572
+ right_options_layout.addStretch()
573
+ right_options.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
574
+
575
+ options_grid.addWidget(self.chk_recursive, 2, 2)
576
+ options_grid.addWidget(right_options, 0, 3, 3, 1, Qt.AlignTop | Qt.AlignLeft)
577
+ search_options_layout.addLayout(options_grid)
578
+
579
+ self.options_tabs.addTab(search_options_tab, "Search options")
580
+
581
+ other_options_tab = QWidget()
582
+ other_options_layout = QGridLayout(other_options_tab)
583
+ other_options_layout.setContentsMargins(8, 8, 8, 8)
584
+ other_options_layout.setHorizontalSpacing(20)
585
+ other_options_layout.setVerticalSpacing(12)
586
+ self.combo_theme = QComboBox()
587
+ self.combo_theme.addItem("System default", "system")
588
+ self.combo_theme.addItem("Dark", "dark")
589
+ self.combo_theme.addItem("Light", "light")
590
+ self.combo_search_engine = QComboBox()
591
+ self.combo_search_engine.addItem(f"xlsxgrep v{xlsxgrep_version}", "xlsxgrep")
592
+ self.combo_jobs = QComboBox()
593
+ cpu_count = os.cpu_count() or 1
594
+ self.combo_jobs.addItem("Auto (All Cores)", 0)
595
+ self.combo_jobs.addItem("1 Core (Sequential)", 1)
596
+ for c in range(2, min(cpu_count + 1, 17)):
597
+ self.combo_jobs.addItem(f"{c} CPU Cores", c)
598
+
599
+ other_options_layout.addWidget(QLabel("Appearance:"), 0, 0)
600
+ other_options_layout.addWidget(self.combo_theme, 0, 1)
601
+ other_options_layout.addWidget(QLabel("Search engine:"), 1, 0)
602
+ other_options_layout.addWidget(self.combo_search_engine, 1, 1)
603
+ other_options_layout.addWidget(QLabel("CPU Usage:"), 2, 0)
604
+ other_options_layout.addWidget(self.combo_jobs, 2, 1)
605
+ other_options_layout.setColumnStretch(1, 1)
606
+
607
+ self.options_tabs.addTab(other_options_tab, "Other Options")
608
+ self.options_tabs.setCurrentIndex(0)
609
+
610
+ # Buttons Row
611
+ row6 = QHBoxLayout()
612
+ row6.addStretch()
613
+
614
+ self.btn_search = QPushButton("Search")
615
+ self.btn_search.setDefault(True)
616
+ self.btn_search.clicked.connect(self.start_search)
617
+ row6.addWidget(self.btn_search)
618
+
619
+ btn_export = QPushButton("Export CSV...")
620
+ btn_export.clicked.connect(self.export_csv)
621
+ row6.addWidget(btn_export)
622
+
623
+ btn_reset_options = QPushButton("Reset options")
624
+ btn_reset_options.clicked.connect(self.reset_options)
625
+ row6.addWidget(btn_reset_options)
626
+
627
+ btn_clear = QPushButton("Clear Results")
628
+ btn_clear.clicked.connect(self.clear_results)
629
+ row6.addWidget(btn_clear)
630
+
631
+ panel_layout.addLayout(row6)
632
+ main_layout.addWidget(panel)
633
+
634
+ # --- Results Table ---
635
+ self.table = QTableWidget(0, 3)
636
+ self.table.setHorizontalHeaderLabels(["File", "Sheet / Position", "Matched Row / Line"])
637
+ self.table.setWordWrap(False)
638
+ self.table.setAlternatingRowColors(True)
639
+ self.reset_results_table_layout()
640
+ self.chk_filename.toggled.connect(self.update_output_columns)
641
+ self.chk_sheetname.toggled.connect(self.update_output_columns)
642
+ self.update_output_columns()
643
+ self.restore_selected_theme()
644
+ self.combo_theme.currentIndexChanged.connect(self.on_theme_changed)
645
+ main_layout.addWidget(self.table)
646
+
647
+ # --- Status Bar & Progress Bar ---
648
+ self.status_bar = QStatusBar()
649
+ self.setStatusBar(self.status_bar)
650
+ self.status_bar.showMessage("Ready.")
651
+
652
+ self.btn_processing_warnings = QPushButton("⚠")
653
+ self.btn_processing_warnings.setFlat(True)
654
+ self.btn_processing_warnings.setVisible(False)
655
+ self.btn_processing_warnings.setToolTip("")
656
+ self.btn_processing_warnings.clicked.connect(self.show_processing_errors_dialog)
657
+ self.status_bar.addPermanentWidget(self.btn_processing_warnings)
658
+
659
+ self.lbl_elapsed_time = QLabel("")
660
+ self.status_bar.addPermanentWidget(self.lbl_elapsed_time)
661
+
662
+ self.progress_bar = QProgressBar()
663
+ self.progress_bar.setRange(0, 100)
664
+ self.progress_bar.setValue(0)
665
+ self.progress_bar.setFormat("%p%")
666
+ self.progress_bar.setVisible(False)
667
+ self.status_bar.addPermanentWidget(self.progress_bar)
668
+
669
+ def browse_file(self):
670
+ file_path, _ = QFileDialog.getOpenFileName(
671
+ self,
672
+ "Select Spreadsheet File",
673
+ self.inp_path.text().strip() or default_documents_directory(),
674
+ "Spreadsheet Files (*.xlsx *.xls *.xlsm *.csv *.tsv *.ods);;All Files (*)",
675
+ )
676
+ if file_path:
677
+ self.inp_path.setText(file_path)
678
+
679
+ def browse_directory(self):
680
+ dir_path = QFileDialog.getExistingDirectory(
681
+ self,
682
+ "Select Folder to Search",
683
+ self.inp_path.text().strip() or default_documents_directory(),
684
+ )
685
+ if dir_path:
686
+ self.inp_path.setText(dir_path)
687
+
688
+ def clear_results(self):
689
+ self.table.setRowCount(0)
690
+ self.reset_results_table_layout()
691
+ self.table.verticalScrollBar().setValue(0)
692
+ self.search_start_time = None
693
+ self.lbl_elapsed_time.setText("")
694
+ self.set_processing_errors([])
695
+ self.status_bar.showMessage("Results cleared.", 3000)
696
+
697
+ def reset_options(self):
698
+ self.chk_ignore_case.setChecked(True)
699
+ self.chk_word_regexp.setChecked(False)
700
+ self.chk_fixed_strings.setChecked(True)
701
+ self.combo_regex_type.setCurrentIndex(0)
702
+ self.chk_count.setChecked(False)
703
+
704
+ self.chk_recursive.setChecked(True)
705
+ self.chk_filename.setChecked(False)
706
+ self.chk_sheetname.setChecked(False)
707
+ self.chk_lmatch.setChecked(False)
708
+ self.chk_lwithout.setChecked(False)
709
+
710
+ self.combo_search_mode.setCurrentIndex(1)
711
+ self.chk_custom_separator.setChecked(False)
712
+ self.inp_sep.setText("\\t")
713
+
714
+ theme_index = self.combo_theme.findData("system")
715
+ if theme_index < 0:
716
+ theme_index = 0
717
+ self.combo_theme.setCurrentIndex(theme_index)
718
+ self.combo_search_engine.setCurrentIndex(0)
719
+
720
+ jobs_index = self.combo_jobs.findData(0)
721
+ if jobs_index < 0:
722
+ jobs_index = 0
723
+ self.combo_jobs.setCurrentIndex(jobs_index)
724
+
725
+ self.options_tabs.setCurrentIndex(0)
726
+ self.status_bar.showMessage("Options reset.", 3000)
727
+
728
+ def reset_results_table_layout(self):
729
+ header = self.table.horizontalHeader()
730
+ self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
731
+ header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
732
+ header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
733
+ header.setSectionResizeMode(2, QHeaderView.Stretch)
734
+ header.setStretchLastSection(True)
735
+ self.table.horizontalScrollBar().setValue(0)
736
+
737
+ def fit_results_table_to_contents(self):
738
+ header = self.table.horizontalHeader()
739
+ self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
740
+ header.setSectionResizeMode(0, QHeaderView.Interactive)
741
+ header.setSectionResizeMode(1, QHeaderView.Interactive)
742
+ header.setSectionResizeMode(2, QHeaderView.Interactive)
743
+ header.setStretchLastSection(False)
744
+ self.table.resizeColumnsToContents()
745
+ self.ensure_file_column_width()
746
+ self.table.horizontalScrollBar().setValue(0)
747
+
748
+ def update_output_columns(self):
749
+ self.table.setColumnHidden(0, not self.chk_filename.isChecked())
750
+ self.table.setColumnHidden(1, not self.chk_sheetname.isChecked())
751
+ self.ensure_file_column_width()
752
+
753
+ def ensure_file_column_width(self):
754
+ if self.table.isColumnHidden(0):
755
+ return
756
+
757
+ header = self.table.horizontalHeader()
758
+ sheet_width = max(self.table.columnWidth(1), header.defaultSectionSize())
759
+ minimum_file_width = sheet_width * 2
760
+ if self.table.columnWidth(0) < minimum_file_width:
761
+ self.table.setColumnWidth(0, minimum_file_width)
762
+
763
+ def on_fixed_strings_toggled(self, checked):
764
+ if checked:
765
+ self.chk_word_regexp.setChecked(False)
766
+ self.combo_regex_type.setCurrentIndex(0)
767
+
768
+ def on_word_regexp_toggled(self, checked):
769
+ if checked:
770
+ self.chk_fixed_strings.setChecked(False)
771
+ self.combo_regex_type.setCurrentIndex(0)
772
+
773
+ def on_regex_type_changed(self, index):
774
+ if self.combo_regex_type.itemData(index) is not None:
775
+ self.chk_fixed_strings.setChecked(False)
776
+ self.chk_word_regexp.setChecked(False)
777
+ self.chk_ignore_case.setChecked(False)
778
+
779
+ def on_ignore_case_toggled(self, checked):
780
+ if checked:
781
+ self.combo_regex_type.setCurrentIndex(0)
782
+
783
+ def on_count_toggled(self, checked):
784
+ self.update_output_columns()
785
+
786
+
787
+ def restore_selected_theme(self):
788
+ saved_theme = normalize_theme_choice(self.settings.value("ui/theme", "system"))
789
+ theme_index = self.combo_theme.findData(saved_theme)
790
+ if theme_index < 0:
791
+ theme_index = 0
792
+ self.combo_theme.setCurrentIndex(theme_index)
793
+ self.apply_selected_theme(saved_theme)
794
+
795
+ def on_theme_changed(self):
796
+ selected_theme = normalize_theme_choice(self.combo_theme.currentData())
797
+ self.apply_selected_theme(selected_theme)
798
+ self.settings.setValue("ui/theme", selected_theme)
799
+
800
+ def apply_selected_theme(self, selected_theme=None):
801
+ app = QApplication.instance()
802
+ selected_theme = normalize_theme_choice(selected_theme or self.combo_theme.currentData())
803
+
804
+ if selected_theme == "dark":
805
+ app.setStyle("Fusion")
806
+ app.setPalette(build_dark_palette())
807
+ app.setStyleSheet("")
808
+ elif selected_theme == "light":
809
+ app.setStyle("Fusion")
810
+ app.setPalette(build_light_palette())
811
+ app.setStyleSheet("")
812
+ else:
813
+ app.setStyle(self.original_style_name)
814
+ app.setPalette(QPalette(self.original_palette))
815
+ app.setStyleSheet(self.original_stylesheet)
816
+
817
+ def set_processing_errors(self, processing_errors):
818
+ self.processing_errors = normalize_processing_errors(processing_errors)
819
+ if self.processing_errors:
820
+ self.btn_processing_warnings.setToolTip(
821
+ format_processing_error_tooltip(len(self.processing_errors))
822
+ )
823
+ self.btn_processing_warnings.setVisible(True)
824
+ else:
825
+ self.btn_processing_warnings.setVisible(False)
826
+ self.btn_processing_warnings.setToolTip("")
827
+
828
+ def show_processing_errors_dialog(self):
829
+ if not self.processing_errors:
830
+ return
831
+
832
+ dialog = QDialog(self)
833
+ dialog.setWindowTitle("Some Files Were Not Processed")
834
+ dialog.resize(int(self.width() * 0.6), int(self.height() * 0.5))
835
+
836
+ layout = QVBoxLayout(dialog)
837
+
838
+ summary_label = QLabel(format_processing_error_summary(len(self.processing_errors)))
839
+ summary_label.setWordWrap(True)
840
+ layout.addWidget(summary_label)
841
+
842
+ info_label = QLabel(
843
+ "Some files may be corrupted, password protected, unsupported, or otherwise unreadable. "
844
+ "Review the log below for the skipped files and reported reasons."
845
+ )
846
+ info_label.setWordWrap(True)
847
+ layout.addWidget(info_label)
848
+
849
+ details_edit = QPlainTextEdit()
850
+ details_edit.setReadOnly(True)
851
+ details_edit.setLineWrapMode(QPlainTextEdit.NoWrap)
852
+ details_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
853
+ details_edit.setPlainText(build_processing_error_details(self.processing_errors))
854
+ layout.addWidget(details_edit)
855
+
856
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok)
857
+ button_box.accepted.connect(dialog.accept)
858
+ layout.addWidget(button_box)
859
+
860
+ dialog.exec()
861
+
862
+ def start_search(self):
863
+ pattern = self.inp_pattern.text().strip()
864
+ if not pattern:
865
+ QMessageBox.warning(self, "Missing Pattern", "Please enter a search pattern.")
866
+ return
867
+
868
+ path_str = self.inp_path.text().strip() or "."
869
+ fixed_strings = self.chk_fixed_strings.isChecked()
870
+ regex_type = self.combo_regex_type.currentData()
871
+ posix_ere = regex_type is True
872
+ regex_enabled = regex_type is not None
873
+ column_mode = self.combo_search_mode.currentData()
874
+ separator = "\t"
875
+ if self.chk_custom_separator.isChecked():
876
+ separator = self.inp_sep.text()
877
+ if separator == "\\t":
878
+ separator = "\t"
879
+
880
+ opts = build_search_options(
881
+ pattern,
882
+ fixed_strings=fixed_strings,
883
+ regex_enabled=regex_enabled,
884
+ posix_ere=posix_ere,
885
+ ignore_case=self.chk_ignore_case.isChecked(),
886
+ word_regexp=self.chk_word_regexp.isChecked(),
887
+ recursive=self.chk_recursive.isChecked(),
888
+ jobs=self.combo_jobs.currentData(),
889
+ count=self.chk_count.isChecked(),
890
+ with_filename=True,
891
+ with_sheetname=True,
892
+ files_with_match=self.chk_lmatch.isChecked(),
893
+ files_without_match=self.chk_lwithout.isChecked(),
894
+ column_mode=column_mode,
895
+ separator=separator,
896
+ debug=False,
897
+ )
898
+
899
+ self.btn_search.setEnabled(False)
900
+ self.progress_bar.setRange(0, 1)
901
+ self.progress_bar.setValue(0)
902
+ self.progress_bar.setVisible(True)
903
+ self.table.setRowCount(0)
904
+ self.reset_results_table_layout()
905
+ self.table.verticalScrollBar().setValue(0)
906
+ self.search_start_time = perf_counter()
907
+ self.lbl_elapsed_time.setText("")
908
+ self.set_processing_errors([])
909
+ self.status_bar.showMessage(f"Searching for '{pattern}' in {path_str}...")
910
+
911
+ # Run search in background thread
912
+ self.worker = SearchWorkerThread(path_str, self.chk_recursive.isChecked(), opts)
913
+ self.worker.result_signal.connect(self.on_search_finished)
914
+ self.worker.progress_signal.connect(self.on_search_progress)
915
+ self.worker.start()
916
+
917
+ def on_search_progress(self, value, maximum, msg):
918
+ self.progress_bar.setRange(0, maximum)
919
+ self.progress_bar.setValue(value)
920
+ self.status_bar.showMessage(msg)
921
+
922
+ def on_search_finished(self, data):
923
+ self.btn_search.setEnabled(True)
924
+ self.progress_bar.setValue(self.progress_bar.maximum())
925
+ self.progress_bar.setVisible(False)
926
+
927
+ elapsed_time = None
928
+ if self.search_start_time is not None:
929
+ elapsed_time = perf_counter() - self.search_start_time
930
+ self.search_start_time = None
931
+ self.lbl_elapsed_time.setText(f"Time: {format_elapsed_time(elapsed_time)}")
932
+
933
+ if "error" in data:
934
+ self.set_processing_errors([])
935
+ QMessageBox.critical(self, "Engine Error", data["error"])
936
+ self.status_bar.showMessage("Error: " + data["error"])
937
+ return
938
+
939
+ self.set_processing_errors(data.get("processing_errors", []))
940
+ results = data["results"]
941
+ self.table.setRowCount(len(results))
942
+ for row_idx, (file_name, sheet_name, content) in enumerate(results):
943
+ self.table.setItem(row_idx, 0, QTableWidgetItem(file_name))
944
+ self.table.setItem(row_idx, 1, QTableWidgetItem(sheet_name))
945
+ self.table.setItem(row_idx, 2, QTableWidgetItem(content))
946
+
947
+ if results:
948
+ self.fit_results_table_to_contents()
949
+ else:
950
+ self.reset_results_table_layout()
951
+ self.table.verticalScrollBar().setValue(0)
952
+ self.status_bar.showMessage(data["status_message"])
953
+
954
+ def export_csv(self):
955
+ row_count = self.table.rowCount()
956
+ if row_count == 0:
957
+ QMessageBox.warning(self, "No Results", "There are no search results to export.")
958
+ return
959
+
960
+ file_path, _ = QFileDialog.getSaveFileName(
961
+ self, "Export Results to CSV", "tdsearch_export.csv", "CSV Files (*.csv)"
962
+ )
963
+ if not file_path:
964
+ return
965
+
966
+ try:
967
+ with open(file_path, "w", newline="", encoding="utf-8") as f:
968
+ writer = csv.writer(f)
969
+ writer.writerow(["File", "Sheet / Position", "Matched Row / Line"])
970
+ for row in range(row_count):
971
+ file_item = self.table.item(row, 0)
972
+ sheet_item = self.table.item(row, 1)
973
+ content_item = self.table.item(row, 2)
974
+ writer.writerow([
975
+ file_item.text() if file_item else "",
976
+ sheet_item.text() if sheet_item else "",
977
+ content_item.text() if content_item else "",
978
+ ])
979
+
980
+ QMessageBox.information(
981
+ self, "Export Successful", f"Successfully exported {row_count} rows to:\n{file_path}"
982
+ )
983
+ except Exception as e:
984
+ QMessageBox.critical(self, "Export Error", f"Failed to export CSV:\n{e}")
985
+
986
+
987
+ def main():
988
+ if not PYSIDE_AVAILABLE:
989
+ print(
990
+ "Error: 'PySide6' is required for TDsearch GUI.\n"
991
+ "Please install it using:\n"
992
+ " pip install PySide6",
993
+ file=sys.stderr,
994
+ )
995
+ sys.exit(1)
996
+
997
+ app = QApplication(sys.argv)
998
+ window = TDSearchGUI()
999
+ window.show()
1000
+ sys.exit(app.exec())
1001
+
1002
+
1003
+ if __name__ == "__main__":
1004
+ main()
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: tdsearch
3
+ Version: 0.1.1
4
+ Summary: Desktop (GUI) for Tabular Data Search powered by xlsxgrep.
5
+ Home-page: https://github.com/zazuum/TDsearch
6
+ Author: Ivan Cvitic
7
+ Author-email: cviticivan@gmail.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/zazuum/TDsearch
10
+ Project-URL: Tracker, https://github.com/zazuum/TDsearch/issues
11
+ Classifier: Environment :: X11 Applications :: Qt
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Environment :: Win32 (MS Windows)
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Office/Business
19
+ Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
20
+ Classifier: Topic :: Utilities
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: xlsxgrep==0.0.34
24
+ Requires-Dist: PySide6>=6.0.0
25
+ Dynamic: license-file
26
+
27
+ # TDsearch (Tabular Data Search)
28
+
29
+ **TDsearch** is a desktop GUI application designed to search text across spreadsheet files including XLSX, XLS, XLSM, CSV, TSV, and ODS formats.
30
+
31
+ ## Features
32
+ - **GUI Interface:** Modern PySide6 interface with file and folder browsers.
33
+ - **Multiprocessing Support:** Multi-core parallel search execution.
34
+ - **Regex Support:** Python regex and POSIX Extended Regular Expressions (-E).
35
+ - **Search Options:** Ignore case (-i), fixed strings (-F), word match (-w), column search mode.
36
+ - **CSV Export:** Export structured search results directly to CSV.
37
+
38
+ ## Installation
39
+ ```sh
40
+ pip install tdsearch
41
+ ```
42
+
43
+ ## Launch
44
+ ```sh
45
+ tdsearch
46
+ ```
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ tdsearch/__init__.py
7
+ tdsearch/app.py
8
+ tdsearch.egg-info/PKG-INFO
9
+ tdsearch.egg-info/SOURCES.txt
10
+ tdsearch.egg-info/dependency_links.txt
11
+ tdsearch.egg-info/entry_points.txt
12
+ tdsearch.egg-info/requires.txt
13
+ tdsearch.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [gui_scripts]
2
+ tdsearch = tdsearch.app:main
@@ -0,0 +1,2 @@
1
+ xlsxgrep==0.0.34
2
+ PySide6>=6.0.0
@@ -0,0 +1 @@
1
+ tdsearch