dirshot 0.1.1__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirshot
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A flexible utility for creating project snapshots and searching for files.
5
5
  Author-email: init-helpful <init.helpful@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/init-helpful/dirshot
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dirshot"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  authors = [
9
9
  { name="init-helpful", email="init.helpful@gmail.com" },
10
10
  ]
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import sys
3
3
  import re
4
+ import time # Imported for the fallback progress bar
4
5
  from pathlib import Path
5
6
  from dataclasses import dataclass, field
6
7
  from typing import List, Optional, Set, Tuple, Callable, NamedTuple, Dict, Any
@@ -13,21 +14,70 @@ try:
13
14
  from tqdm import tqdm
14
15
  except ImportError:
15
16
 
17
+ # Define a functional fallback dummy tqdm class if the import fails.
16
18
  class tqdm:
17
- def __init__(self, iterable=None, **kwargs):
19
+ """A simple, text-based progress bar fallback if tqdm is not installed."""
20
+
21
+ def __init__(self, iterable=None, total=None, desc="", unit="it", **kwargs):
18
22
  self.iterable = iterable
23
+ self.total = (
24
+ total
25
+ if total is not None
26
+ else (len(iterable) if hasattr(iterable, "__len__") else None)
27
+ )
28
+ self.desc = desc
29
+ self.unit = unit
30
+ self.current = 0
31
+ self.start_time = time.time()
32
+ self._last_update_time = 0
19
33
 
20
34
  def __iter__(self):
21
- return iter(self.iterable)
35
+ for obj in self.iterable:
36
+ yield obj
37
+ self.update(1)
38
+ # The loop is finished, ensure the bar is 100% and close
39
+ if self.total is not None and self.current < self.total:
40
+ self.update(self.total - self.current)
41
+ self.close()
22
42
 
23
43
  def update(self, n=1):
24
- pass
25
-
26
- def set_description(self, desc):
27
- pass
44
+ """Update the progress bar by n steps."""
45
+ self.current += n
46
+ now = time.time()
47
+ # Throttle screen updates to prevent flickering and performance loss
48
+ if (
49
+ self.total is None
50
+ or now - self._last_update_time > 0.1
51
+ or self.current == self.total
52
+ ):
53
+ self._last_update_time = now
54
+ self._draw()
55
+
56
+ def set_description(self, desc: str):
57
+ """Set the description of the progress bar."""
58
+ self.desc = desc
59
+ self._draw()
60
+
61
+ def _draw(self):
62
+ """Draw the progress bar to the console."""
63
+ if self.total:
64
+ percent = int((self.current / self.total) * 100)
65
+ bar_length = 25
66
+ filled_length = int(bar_length * self.current // self.total)
67
+ bar = "█" * filled_length + "-" * (bar_length - filled_length)
68
+ # Use carriage return to print on the same line
69
+ progress_line = f"\r{self.desc}: {percent}%|{bar}| {self.current}/{self.total} [{self.unit}]"
70
+ sys.stdout.write(progress_line)
71
+ else: # Case where total is not known
72
+ sys.stdout.write(f"\r{self.desc}: {self.current} {self.unit}")
73
+
74
+ sys.stdout.flush()
28
75
 
29
76
  def close(self):
30
- pass
77
+ """Clean up the progress bar line."""
78
+ # Print a newline to move off the progress bar line
79
+ sys.stdout.write("\n")
80
+ sys.stdout.flush()
31
81
 
32
82
 
33
83
  # --- Configuration Constants ---
@@ -457,33 +507,36 @@ def _collate_content_to_file(
457
507
  )
458
508
  else: # ProjectMode.SEARCH
459
509
  stats_key = (
460
- "Key: [M: Matched files/dirs]\n"
461
- " (f=files, d=directories)\n\n"
510
+ "Key: [M: Matched files/dirs]\n" " (f=files, d=directories)\n\n"
462
511
  )
463
512
  buffer.write(stats_key)
464
513
  tree_content = "\n".join(tree_content_lines)
465
514
  buffer.write(tree_content + "\n")
466
515
  buffer.write(f"\n{separator_line}\n\n")
467
516
 
468
- for file_info in files_to_process:
469
- header_content = f"{separator_line}\n{FILE_HEADER_PREFIX}{file_info.relative_path_posix}\n{separator_line}\n\n"
470
- buffer.write(header_content)
471
- try:
472
- with open(
473
- file_info.absolute_path, "r", encoding=encoding, errors="replace"
474
- ) as infile:
475
- file_content = infile.read()
476
- buffer.write(file_content)
477
- buffer.write("\n\n")
478
- except Exception:
479
- buffer.write(
480
- f"Error: Could not read file '{file_info.relative_path_posix}'.\n\n"
481
- )
482
-
483
- if not files_to_process and not tree_content_lines:
484
- buffer.write(
485
- "No files found matching the specified criteria for content aggregation.\n"
517
+ # This message is for the file content, not the console.
518
+ if not files_to_process:
519
+ message = (
520
+ "No files found matching the specified criteria.\n"
521
+ if mode == ProjectMode.SEARCH
522
+ else "No files found matching the specified criteria for content aggregation.\n"
486
523
  )
524
+ buffer.write(message)
525
+ else:
526
+ for file_info in files_to_process:
527
+ header_content = f"{separator_line}\n{FILE_HEADER_PREFIX}{file_info.relative_path_posix}\n{separator_line}\n\n"
528
+ buffer.write(header_content)
529
+ try:
530
+ with open(
531
+ file_info.absolute_path, "r", encoding=encoding, errors="replace"
532
+ ) as infile:
533
+ file_content = infile.read()
534
+ buffer.write(file_content)
535
+ buffer.write("\n\n")
536
+ except Exception:
537
+ buffer.write(
538
+ f"Error: Could not read file '{file_info.relative_path_posix}'.\n\n"
539
+ )
487
540
 
488
541
  # Get the complete content from the buffer
489
542
  final_content = buffer.getvalue()
@@ -506,17 +559,22 @@ def _collate_content_to_file(
506
559
  # Write the main content
507
560
  outfile.write(final_content)
508
561
  except IOError as e:
509
- print(f"Error: Could not write to output file '{output_file_path}': {e}")
562
+ print(f"\nError: Could not write to output file '{output_file_path}': {e}")
510
563
  return
511
564
 
512
- # Final console output remains for user feedback
513
- print(f"\nProcess complete. Output written to: {output_file_path}")
565
+ # Final console output for user feedback
566
+ if mode == ProjectMode.SEARCH:
567
+ if files_to_process:
568
+ print("\nSuccess! Collation complete.")
569
+ else: # Filter mode has its own messaging pattern
570
+ print(f"\nProcess complete. Output written to: {output_file_path}")
571
+ if len(files_to_process) > 0:
572
+ print(
573
+ f"Summary: {len(files_to_process)} files selected for content processing."
574
+ )
575
+
514
576
  if show_token_count:
515
577
  print(f"Total Approximated Tokens ({mode_display}): {total_token_count}")
516
- if len(files_to_process) > 0:
517
- print(
518
- f"Summary: {len(files_to_process)} files selected for content processing."
519
- )
520
578
 
521
579
 
522
580
  def filter_and_append_content(
@@ -611,19 +669,43 @@ def search_and_collate_content(
611
669
  if not normalized_keywords:
612
670
  print("Error: Search mode requires 'search_keywords' to be provided.")
613
671
  return
672
+
673
+ print("Phase 1: Finding all matching files...")
674
+ if criteria.ignore_path_components:
675
+ print(
676
+ f"Ignoring directories and files containing: {', '.join(criteria.ignore_path_components)}"
677
+ )
678
+
614
679
  candidate_files: List[Path] = []
615
680
  for dirpath_str, dirnames, filenames in os.walk(str(root_dir), topdown=True):
616
681
  current_dir_path = Path(dirpath_str)
682
+ # Prune directories based on ignore criteria
617
683
  dirnames[:] = [
618
- d for d in dirnames if d.lower() not in criteria.ignore_path_components
684
+ d
685
+ for d in dirnames
686
+ if (current_dir_path / d).name.lower()
687
+ not in criteria.ignore_path_components
619
688
  ]
689
+
620
690
  for filename in filenames:
621
691
  file_abs_path = current_dir_path / filename
692
+ # Also ignore individual files based on path components
693
+ try:
694
+ relative_parts = file_abs_path.relative_to(root_dir).parts
695
+ if any(
696
+ part.lower() in criteria.ignore_path_components
697
+ for part in relative_parts
698
+ ):
699
+ continue
700
+ except ValueError:
701
+ continue
702
+
622
703
  if (
623
- file_abs_path.suffix.lower() in criteria.file_extensions
624
- or not criteria.file_extensions
704
+ not criteria.file_extensions
705
+ or file_abs_path.suffix.lower() in criteria.file_extensions
625
706
  ):
626
707
  candidate_files.append(file_abs_path)
708
+
627
709
  matched_files: Set[Path] = set()
628
710
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
629
711
  future_to_file = {
@@ -646,23 +728,21 @@ def search_and_collate_content(
646
728
  result = future.result()
647
729
  if result:
648
730
  matched_files.add(result)
731
+
649
732
  if not matched_files:
650
733
  print("\nScan complete. No matching files were found.")
651
- _collate_content_to_file(
652
- output_file,
653
- None,
654
- [],
655
- DEFAULT_ENCODING,
656
- DEFAULT_SEPARATOR_CHAR,
657
- DEFAULT_SEPARATOR_LINE_LENGTH,
658
- show_token_count,
659
- show_tree_stats,
660
- ProjectMode.SEARCH,
661
- )
734
+ # Still create the output file with a "not found" message
735
+ with open(output_file, "w", encoding=DEFAULT_ENCODING) as f_out:
736
+ f_out.write("No files found matching the specified criteria.\n")
662
737
  return
738
+
663
739
  sorted_matched_files = sorted(
664
740
  list(matched_files), key=lambda p: p.relative_to(root_dir).as_posix().lower()
665
741
  )
742
+
743
+ print(f"\nPhase 1 Complete: Found {len(sorted_matched_files)} matching files.")
744
+ print(f"\nPhase 2: Generating output file at '{Path(output_file).resolve()}'...")
745
+
666
746
  tree_content_lines = _generate_tree_from_paths(
667
747
  root_dir, sorted_matched_files, tree_style, show_tree_stats
668
748
  )
@@ -928,4 +1008,4 @@ if __name__ == "__main__":
928
1008
  ignore_dirs_in_path=["venv", "build", "node_modules", "static", "templates"],
929
1009
  show_tree_stats=True,
930
1010
  show_token_count=True,
931
- )
1011
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirshot
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A flexible utility for creating project snapshots and searching for files.
5
5
  Author-email: init-helpful <init.helpful@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/init-helpful/dirshot
File without changes
File without changes
File without changes
File without changes
File without changes