-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFileFind.py
More file actions
814 lines (652 loc) Β· 30 KB
/
FileFind.py
File metadata and controls
814 lines (652 loc) Β· 30 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
#!/usr/bin/env python3
"""
FileFind - File Search Tool
Search using Trie data structures and multi-strategy algorithms.
Built for Windows.
"""
import os
import time
from collections import defaultdict
from pathlib import Path
from typing import Any, List, Optional, Tuple
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich.prompt import Prompt, Confirm
from rich import box
# Initialize Rich console for beautiful terminal output
console = Console()
# CONSTANTS - Centralized configuration for easy maintenance
# System directories to skip during indexing (improves performance and security)
SKIP_DIRECTORIES = {
"system32",
"windows",
"programdata",
"$recycle",
"appdata",
".git",
"node_modules",
"__pycache__",
}
# Search configuration constants
MIN_WORD_LENGTH = 2 # Minimum word length to index (skip short words like 'a', 'of')
MAX_FILENAME_SCORE_BONUS = 30 # Maximum bonus for shorter filenames in relevance scoring
DEFAULT_SEARCH_RESULTS = 50 # Default number of search results to return
DISPLAY_RESULTS_LIMIT = 20 # Maximum results to display in table
# Relevance scoring weights (higher = more relevant)
SCORE_EXACT_MATCH = 100 # Query exactly matches filename
SCORE_STARTS_WITH = 80 # Filename starts with query (autocomplete-style)
SCORE_CONTAINS = 50 # Filename contains query somewhere
# UTILITY CLASSES - Reusable components for common operations
class PathUtils:
"""Safe path operations with security validation and drive detection."""
@staticmethod
def get_drive_path(drive_letter: str) -> Path:
"""Convert drive letter to Path object (e.g., 'D' -> 'D:/')"""
return Path(f"{drive_letter.upper()}:/")
@staticmethod
def get_available_drives() -> List[str]:
"""Return list of accessible drive letters (C, D, E, etc.)."""
drives = []
for letter in "CDEFGHIJKLMNOPQRSTUVWXYZ":
if PathUtils.get_drive_path(letter).exists():
drives.append(letter)
return drives
@staticmethod
def is_valid_folder(path: Path) -> bool:
"""Check if path exists and is actually a directory (not a file)"""
return path.exists() and path.is_dir()
@staticmethod
def should_skip_directory(path: Path) -> bool:
"""Return True if directory should be skipped (system folders, node_modules, etc.)."""
path_parts = [p.lower() for p in path.parts]
return any(skip in path_parts for skip in SKIP_DIRECTORIES)
@staticmethod
def is_safe_filename(name: str) -> bool:
"""Validate filename blocks directory traversal and Windows reserved names."""
# Check for empty or whitespace-only names
if not name or not name.strip():
return False
# Directory traversal protection - these patterns can escape intended directories
if ".." in name or "\\" in name:
return False
# Windows file system restrictions - these characters cause errors
# '/' is included to block path traversal during rename (e.g. "sub/file")
invalid_chars = '<>:"|?*/'
if any(char in name for char in invalid_chars):
return False
# Windows reserved names (cannot be used as file/folder names)
reserved_names = {
"con", "prn", "aux", "nul",
"com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",
"lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
}
base_name = name.split(".")[0].lower()
if base_name in reserved_names:
return False
# Windows doesn't allow names ending with periods or spaces
if name.endswith(".") or name.endswith(" "):
return False
# Windows filename length limit (255 characters)
if len(name) > 255:
return False
return True
@staticmethod
def get_item_type(path: Path) -> str:
"""Get simple item type string: 'folder' or 'file'"""
return "folder" if path.is_dir() else "file"
@staticmethod
def get_item_emoji_type(path: Path) -> str:
"""Get emoji item type string: 'π Folder' or 'π File'"""
return "π Folder" if path.is_dir() else "π File"
class UIUtils:
"""Terminal UI helpers for tables, menus, messages, and user input."""
@staticmethod
def create_results_table(title: str, columns: List[Tuple[str, str, int]]) -> Table:
"""Create styled Rich table with title and columns (name, style, width). Width 0 = auto-size."""
table = Table(title=title, show_lines=True, header_style="bold cyan")
for name, style, width in columns:
if width:
table.add_column(name, style=style, width=width)
else:
table.add_column(name, style=style)
return table
@staticmethod
def get_user_choice(prompt: str, choices: List[str], default: Optional[str] = None) -> str:
"""Get validated user input with automatic retry on invalid choices"""
if default:
return Prompt.ask(prompt, choices=choices, default=default)
else:
return Prompt.ask(prompt, choices=choices)
@staticmethod
def show_options_and_choose(options: List[str], prompt: str) -> str:
"""Display numbered options and return validated user choice."""
for option in options:
console.print(option)
choices = [str(i) for i in range(1, len(options) + 1)]
return UIUtils.get_user_choice(prompt, choices)
@staticmethod
def print_success(message: str):
"""Print success message with consistent formatting"""
console.print(f"[bold green]β
SUCCESS:[/] {message}")
@staticmethod
def print_error(message: str):
"""Print error message with consistent formatting"""
console.print(f"[bold red]β ERROR:[/] {message}")
@staticmethod
def print_warning(message: str):
"""Print warning message with consistent formatting"""
console.print(f"[bold yellow]β οΈ WARNING:[/] {message}")
@staticmethod
def print_info(message: str):
"""Print info message with consistent formatting"""
console.print(f"[bold cyan]βΉοΈ INFO:[/] {message}")
@staticmethod
def print_separator():
"""Print standard visual separator line"""
console.print("β" * 60)
@staticmethod
def print_section_break():
"""Print section break line for major divisions"""
console.print("β" * 60)
@staticmethod
def print_section_header(title: str):
"""Print formatted section header with consistent styling"""
console.print()
console.print(Panel(title, style="bold green"))
UIUtils.print_separator()
@staticmethod
def validate_filename_or_show_error(name: str) -> bool:
"""Validate filename and print error if invalid. Returns True if valid."""
if not PathUtils.is_safe_filename(name):
UIUtils.print_error(
"Invalid name. Avoid empty names, '..' patterns, and special characters"
)
return False
return True
@staticmethod
def safe_execute(operation_name: str, func, *args, **kwargs) -> Any:
"""Execute function with error handling. Catches file system errors."""
try:
return func(*args, **kwargs)
except PermissionError:
UIUtils.print_error(f"Permission denied: {operation_name}")
except FileNotFoundError:
UIUtils.print_error(f"File not found: {operation_name}")
except FileExistsError:
UIUtils.print_error(f"File already exists: {operation_name}")
except OSError as e:
UIUtils.print_error(f"{operation_name} - {e}")
return None
# SEARCH ENGINE - Fast file indexing and retrieval system
class TrieNode:
"""Trie node storing children (char -> TrieNode) and files matching this prefix."""
def __init__(self):
self.children = {} # Dictionary mapping characters to child nodes
self.files = [] # Files that contain this prefix
class Trie:
"""Prefix tree for O(m) prefix matching where m = query length."""
def __init__(self):
self.root = TrieNode()
def insert(self, word: str, file_path: Path):
"""Insert word into trie. Adds file_path to every prefix node for partial matching."""
node = self.root
for char in word.lower():
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
# Add file to this prefix - enables partial matching
node.files.append(file_path)
def search_prefix(self, prefix: str, max_results: int = 20) -> List[Path]:
"""Find unique files matching prefix. Returns up to max_results."""
node = self.root
for char in prefix.lower():
if char not in node.children:
return [] # Prefix not found
node = node.children[char]
# Remove duplicates while preserving order (dict.fromkeys trick)
unique_files = list(dict.fromkeys(node.files))
return unique_files[:max_results]
class FileMetadata:
"""Cached file info (path, name, suffix, is_dir) to avoid repeated Path operations."""
def __init__(self, path: Path):
self.path = path
self.name = path.name
self.suffix = path.suffix.lower() # File extension for type filtering
self.is_dir = path.is_dir()
class FileSearchIndex:
"""Multi-strategy search: exact match (O(1)), Trie prefix (O(m)), word index, substring."""
def __init__(self):
# Trie for fast prefix search (like autocomplete)
self.trie = Trie()
# Hash map for instant exact filename lookup
self.exact_match = {} # filename -> [FileMetadata]
# Inverted index: word -> set of files containing that word
# Enables searching for "intern the" to find "The Intern.mp4"
self.word_index = defaultdict(set)
# Track indexed files to avoid duplicates
self.indexed_paths = set()
# Statistics for user feedback
self.total_items = 0
@staticmethod
def _tokenize(text: str) -> List[str]:
"""Split text into searchable words on dots, underscores, and dashes."""
return text.replace(".", " ").replace("_", " ").replace("-", " ").split()
def add_file(self, file_path: Path):
"""Index file/folder in Trie, exact_match, and word_index. Skips duplicates."""
# Avoid duplicate indexing (important for performance)
if str(file_path).lower() in self.indexed_paths:
return
try:
metadata = FileMetadata(file_path)
filename = metadata.name.lower()
# 1. Add to trie for prefix search
self.trie.insert(filename, file_path)
# 2. Add to exact match lookup
if filename not in self.exact_match:
self.exact_match[filename] = []
self.exact_match[filename].append(metadata)
# 3. Add to word index for flexible search
# Split filename into searchable words (handle dots, underscores, dashes)
words = FileSearchIndex._tokenize(filename)
for word in words:
if len(word) > MIN_WORD_LENGTH: # Skip very short words (the, of, a, etc.)
self.word_index[word].add(file_path)
# Track this file as indexed
self.indexed_paths.add(str(file_path).lower())
self.total_items += 1
except (OSError, PermissionError):
# Skip files we can't access (common in system directories)
pass
def index_folder(self, folder_path: Path) -> int:
"""Recursively index all files/folders in directory. Returns count of items indexed."""
items_added = 0
if not PathUtils.is_valid_folder(folder_path):
return items_added
try:
# rglob("*") recursively finds all files AND folders in subdirectories
for item in folder_path.rglob("*"):
# Skip symlinks and NTFS junctions to avoid traversing outside
# the intended scope (e.g. OneDrive junctions, dev env mounts)
if item.is_symlink():
continue
# Skip system directories for performance and security
if PathUtils.should_skip_directory(item.parent):
continue
# Index both files AND folders for comprehensive search
self.add_file(item) # Works for both files and directories
items_added += 1
except (OSError, PermissionError):
# Skip inaccessible directories (network drives, system folders, etc.)
pass
return items_added
def search(self, query: str, max_results: int = 20) -> List[Path]:
"""Search using 4 strategies: exact, prefix, word, substring. Returns top results by relevance."""
if not query.strip():
return []
query = query.lower().strip()
results = set() # Use set to automatically handle duplicates
# Strategy 1: Exact filename match (fastest possible)
if query in self.exact_match:
for metadata in self.exact_match[query]:
results.add(metadata.path)
# Strategy 2: Prefix search using Trie (autocomplete-style)
prefix_results = self.trie.search_prefix(query, max_results * 2)
results.update(prefix_results)
# Strategy 3: Word-based search (handles different word orders)
# Splits "the intern" into ["the", "intern"] for flexible matching
query_words = FileSearchIndex._tokenize(query)
for word in query_words:
if word in self.word_index:
results.update(self.word_index[word])
# Strategy 4: Substring search (broadest, slowest)
# Only use if we don't have enough results yet
if len(results) < max_results:
for filename, metadata_list in self.exact_match.items():
if query in filename:
for metadata in metadata_list:
results.add(metadata.path)
# Sort results by relevance and return top matches
return self._sort_by_relevance(list(results), query)[:max_results]
def _sort_by_relevance(self, results: List[Path], query: str) -> List[Path]:
"""Sort by score: exact match > starts with > contains > shorter names > common dirs."""
def score(path: Path) -> int:
filename = path.name.lower()
relevance_score = 0
# Exact match gets highest priority
if query == filename:
relevance_score += SCORE_EXACT_MATCH
# Starts with query (like autocomplete)
elif filename.startswith(query):
relevance_score += SCORE_STARTS_WITH
# Contains query somewhere
elif query in filename:
relevance_score += SCORE_CONTAINS
# Shorter filenames often more relevant (less clutter)
relevance_score += max(0, MAX_FILENAME_SCORE_BONUS - len(filename))
# Bonus for files in commonly-accessed directories
parent_name = path.parent.name.lower()
if any(
common in parent_name
for common in ["documents", "desktop", "downloads"]
):
relevance_score += 10
return relevance_score
return sorted(results, key=score, reverse=True)
# MAIN APPLICATION - Interactive file management interface
class FileCommander:
"""Interactive file search application with Trie-based indexing and multi-strategy search."""
def __init__(self):
self.search_index = FileSearchIndex()
self._index_built = False # Cache flag to avoid re-indexing
def show_main_menu(self):
"""Display the main application menu with available operations."""
console.clear()
# Centered application header with gradient-style colors
title = Text()
title.append("β‘ ", style="bold yellow")
title.append("FILE COMMANDER", style="bold bright_cyan")
subtitle = Text("High-Performance File Search Engine", style="dim white")
# Create header panel with rounded borders
header_content = Text.assemble(
title, "\n", subtitle
)
header_content.justify = "center"
console.print()
console.print(
Panel(
header_content,
box=box.ROUNDED,
style="cyan",
padding=(1, 4),
),
justify="center",
)
console.print()
# Main menu options
options = [
("1", "β‘", "Search", "Find and manage files"),
("2", "π", "Statistics", "View search index status"),
("0", "β", "Exit", "Close application"),
]
# Create styled table with rounded box
table = Table(
box=box.ROUNDED,
show_header=True,
header_style="bold bright_cyan",
border_style="dim cyan",
padding=(0, 1),
)
table.add_column("", style="bold yellow", width=3, justify="center")
table.add_column("", width=3, justify="center")
table.add_column("Action", style="bold white", min_width=20)
table.add_column("Description", style="dim", min_width=25)
for key, icon, action, desc in options:
if key == "0":
table.add_row(
f"[red]{key}[/red]",
icon,
f"[red]{action}[/red]",
f"[dim red]{desc}[/dim red]",
)
else:
table.add_row(key, icon, action, desc)
# Use a grid to center the table robustly
grid = Table.grid(expand=True)
grid.add_column(justify="center")
grid.add_row(table)
console.print(grid)
console.print()
def search_files(self):
"""Index drives (once), then continuous search loop. Actions: open, rename, search again."""
UIUtils.print_section_header("β‘ Search & Manage Files/Folders")
# Only build index if not already cached
if not self._index_built:
# Build index with smart drive strategy
console.print("[dim]π Indexing files using smart drive strategy...[/dim]")
# Strategy 1: C: drive - targeted indexing (common user folders only)
c_drive_folders = [
Path.home() / "Downloads",
Path.home() / "Documents",
Path.home() / "Desktop",
Path.home() / "Videos",
Path.home() / "Pictures",
Path.home() / "Pictures" / "Samsung Flow", # Phone sync location
]
console.print("[dim] π― C: drive - Indexing user folders only...[/dim]")
for folder in c_drive_folders:
if PathUtils.is_valid_folder(folder):
items_added = self.search_index.index_folder(folder)
if items_added > 0:
console.print(
f"[dim] β
{folder.name}: {items_added} items[/dim]"
)
# Strategy 2: Other drives (D:, E:, Z:, etc.) - complete indexing
drives = PathUtils.get_available_drives()
other_drives = [drive for drive in drives if drive.upper() != "C"]
if other_drives:
console.print(
f"[dim] πΎ Other drives ({', '.join(other_drives)}) - Complete indexing...[/dim]"
)
for drive in other_drives:
drive_path = PathUtils.get_drive_path(drive)
console.print(
f"[dim] π Indexing {drive}: drive completely...[/dim]"
)
items_added = self.search_index.index_folder(drive_path)
if items_added > 0:
console.print(
f"[dim] β
{drive}: drive: {items_added} items indexed[/dim]"
)
else:
console.print(
f"[dim] β οΈ {drive}: drive: No accessible items[/dim]"
)
else:
console.print("[dim] βΉοΈ No additional drives found besides C:[/dim]")
UIUtils.print_success("Indexing complete")
self._index_built = True # Mark index as built
else:
UIUtils.print_info("Using cached index (instant search ready)")
UIUtils.print_separator()
# Continuous search loop - no re-indexing needed
while True:
search_term = Prompt.ask("β‘ What are you looking for?")
if not search_term.strip():
UIUtils.print_error("Please enter a search term")
continue # Ask again without breaking the loop
UIUtils.print_info(f"Searching for '{search_term}'...")
# Perform search with performance tracking
start_time = time.time()
results = self.search_index.search(search_term, DEFAULT_SEARCH_RESULTS)
search_time = time.time() - start_time
if results:
UIUtils.print_success(
f"Found {len(results)} results in {search_time:.3f} seconds"
)
UIUtils.print_section_break()
self._display_search_results(results, search_term)
# Handle actions and check if user wants to continue
if not self._handle_search_actions(results):
break # Exit to main menu if user chose "Back to menu"
else:
UIUtils.print_section_break()
UIUtils.print_warning(f"No items found for '{search_term}'")
UIUtils.print_section_break()
# Ask if user wants to continue searching (only when no results)
UIUtils.print_separator()
if not Confirm.ask(
"[bold cyan]π Do you want to search for something else?[/bold cyan]",
default=False,
):
console.print("[dim]π Returning to main menu[/dim]")
break # Exit the search loop and return to main menu
UIUtils.print_separator() # Visual separator for next search
def _display_search_results(self, results: List[Path], search_term: str):
"""Display search results in a formatted table with file type indicators."""
UIUtils.print_separator()
table = UIUtils.create_results_table(
f"π Results for '{search_term}'",
[
("#", "white", 3),
("Name", "green", 0),
("Type", "white", 8),
("Location", "blue", 0),
],
)
# Show first results to avoid overwhelming the user
for i, item in enumerate(results[:DISPLAY_RESULTS_LIMIT], 1):
item_type = PathUtils.get_item_emoji_type(item)
table.add_row(str(i), item.name, item_type, str(item.parent))
console.print(table)
# Indicate if there are more results
if len(results) > DISPLAY_RESULTS_LIMIT:
console.print(
f"[dim]... and {len(results) - DISPLAY_RESULTS_LIMIT} more results (showing first {DISPLAY_RESULTS_LIMIT})[/dim]"
)
UIUtils.print_separator()
def _handle_search_actions(self, results: List[Path]) -> bool:
"""Show action menu. Returns True to continue searching, False to exit to main menu."""
actions = [
"1. π Open item",
"2. βοΈ Rename item",
"3. π Search again",
"4. π Back to menu",
]
action = UIUtils.show_options_and_choose(actions, "Choose action")
if action in ["1", "2"]:
# Get user selection for the action
if len(results) == 1:
selected = results[0]
else:
choice = UIUtils.get_user_choice(
"Enter number",
[str(i) for i in range(1, min(len(results), DISPLAY_RESULTS_LIMIT) + 1)],
)
selected = results[int(choice) - 1]
# Perform the selected action
if action == "1":
self._open_item(selected)
else:
self._rename_item(selected)
return True # Continue searching after open/rename
elif action == "3":
return True # β
Continue search loop (no re-indexing!)
else:
return False # Back to main menu
def _open_item(self, item_path: Path):
"""Open file/folder with os.startfile (safe, no shell injection)."""
def open_operation():
# os.startfile works for both files and folders on Windows
os.startfile(str(item_path))
item_type = PathUtils.get_item_type(item_path)
UIUtils.print_success(f"Opened {item_type}: {item_path.name}")
UIUtils.safe_execute("opening item", open_operation)
def _rename_item(self, item_path: Path):
"""Rename file/folder with validation. Offers undo after successful rename."""
UIUtils.print_section_break()
console.print(Panel(f"βοΈ Rename: {item_path.name}", style="bold cyan"))
UIUtils.print_section_break()
new_name = Prompt.ask("π Enter new name", default=item_path.name)
if new_name == item_path.name:
UIUtils.print_warning("Name unchanged")
return
# Security validation
if not UIUtils.validate_filename_or_show_error(new_name):
return
# Store original info for potential undo
original_path = item_path
original_name = item_path.name
new_path = item_path.parent / new_name
def rename_operation():
try:
original_path.rename(new_path)
item_type = PathUtils.get_item_type(new_path)
UIUtils.print_success(f"Renamed {item_type} to: {new_name}")
return True
except FileExistsError:
UIUtils.print_error(f"Name already exists: {new_name}")
return False
# Guard: ensure the resolved new path stays in the same directory.
# Catches any edge case where a name could slip out of the parent folder.
if new_path.parent.resolve() != item_path.parent.resolve():
UIUtils.print_error(
"Rename cannot move a file to a different directory. "
"Use a plain name without slashes."
)
return
# Perform rename operation
rename_successful = UIUtils.safe_execute("renaming item", rename_operation)
# If rename was successful, offer immediate undo option
if rename_successful:
UIUtils.print_separator()
if Confirm.ask(
"[bold cyan]π Do you want to undo this rename?[/bold cyan]",
default=False,
):
def undo_operation():
new_path.rename(original_path)
item_type = PathUtils.get_item_type(original_path)
UIUtils.print_success(f"Restored original name: {original_name}")
UIUtils.safe_execute("undoing rename", undo_operation)
UIUtils.print_section_break()
def show_search_statistics(self):
"""Display current search index statistics for user information."""
UIUtils.print_section_header("π Search Statistics")
table = UIUtils.create_results_table(
"β‘ Search System Status",
[("Metric", "cyan", 20), ("Value", "green", 20), ("Details", "dim", 40)],
)
# Show indexing status and performance metrics
table.add_row("Status", "β
Ready", "Optimized for instant search")
table.add_row(
"Items Indexed",
f"{self.search_index.total_items:,}",
"Total files and folders in search index",
)
table.add_row("Search Speed", "< 1ms", "Microsecond-level performance")
console.print(table)
UIUtils.print_section_break()
def run_interactive(self):
"""Main application loop. Shows menu and runs search or statistics based on user choice."""
while True:
try:
self.show_main_menu()
choice = UIUtils.get_user_choice(
"Select option", ["0", "1", "2"]
)
if choice == "0":
UIUtils.print_section_break()
console.print(
"[bold yellow]π GOODBYE![/] Thank you for using File Commander"
)
UIUtils.print_section_break()
break
elif choice == "1":
self.search_files()
elif choice == "2":
self.show_search_statistics()
# Pause before returning to menu (better UX)
if choice != "0":
UIUtils.print_separator()
Prompt.ask(
"[dim]Press Enter to return to main menu[/dim]", default=""
)
UIUtils.print_separator()
except KeyboardInterrupt:
# Graceful handling of Ctrl+C
UIUtils.print_section_break()
console.print("[bold yellow]π GOODBYE![/] Interrupted by user")
UIUtils.print_section_break()
break
except Exception as e:
# Unexpected error handling
UIUtils.print_section_break()
UIUtils.print_error(f"Unexpected error: {e}")
console.print("[dim]Please try again or restart the application.[/dim]")
UIUtils.print_section_break()
# APPLICATION ENTRY POINT
if __name__ == "__main__":
commander = FileCommander()
commander.run_interactive()