kopipasta 0.34.0__py3-none-any.whl → 0.35.0__py3-none-any.whl

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.

Potentially problematic release.


This version of kopipasta might be problematic. Click here for more details.

@@ -20,9 +20,12 @@ class FileNode:
20
20
  self.parent = parent
21
21
  self.children: List['FileNode'] = []
22
22
  self.expanded = False
23
- # This flag marks the invisible root of the file tree, which is not meant to be displayed.
24
23
  self.is_scan_root = is_scan_root
24
+ # Base size (for files) or initial placeholder (for dirs)
25
25
  self.size = 0 if is_dir else os.path.getsize(self.path)
26
+ # New attributes for caching the results of a deep scan
27
+ self.total_size: int = self.size
28
+ self.is_scanned: bool = not self.is_dir
26
29
 
27
30
  @property
28
31
  def name(self):
@@ -48,7 +51,41 @@ class TreeSelector:
48
51
  self.char_count = 0
49
52
  self.quit_selection = False
50
53
  self.viewport_offset = 0 # First visible item index
54
+ self._metrics_cache: Dict[str, Tuple[int, int]] = {}
55
+
56
+ def _calculate_directory_metrics(self, node: FileNode) -> Tuple[int, int]:
57
+ """Recursively calculate total and selected size for a directory."""
58
+ if not node.is_dir:
59
+ return 0, 0
51
60
 
61
+ # Use instance cache for this render cycle
62
+ if node.path in self._metrics_cache:
63
+ return self._metrics_cache[node.path]
64
+
65
+ total_size = 0
66
+ selected_size = 0
67
+
68
+ # Ensure directory is scanned
69
+ if not node.children:
70
+ self._deep_scan_directory_and_calc_size(node.path, node)
71
+
72
+ for child in node.children:
73
+ if child.is_dir:
74
+ child_total, child_selected = self._calculate_directory_metrics(child)
75
+ total_size += child_total
76
+ selected_size += child_selected
77
+ else: # It's a file
78
+ total_size += child.size
79
+ if child.path in self.selected_files:
80
+ is_snippet, _ = self.selected_files[child.path]
81
+ if is_snippet:
82
+ selected_size += len(get_file_snippet(child.path))
83
+ else:
84
+ selected_size += child.size
85
+
86
+ self._metrics_cache[node.path] = (total_size, selected_size)
87
+ return total_size, selected_size
88
+
52
89
  def build_tree(self, paths: List[str]) -> FileNode:
53
90
  """Build tree structure from given paths."""
54
91
  # If one directory is given, make its contents the top level of the tree.
@@ -56,7 +93,7 @@ class TreeSelector:
56
93
  root_path = os.path.abspath(paths[0])
57
94
  root = FileNode(root_path, True, is_scan_root=True)
58
95
  root.expanded = True
59
- self._scan_directory(root_path, root)
96
+ self._deep_scan_directory_and_calc_size(root_path, root)
60
97
  return root
61
98
 
62
99
  # Otherwise, create a virtual root to hold multiple items (e.g., `kopipasta file.py dir/`).
@@ -79,7 +116,7 @@ class TreeSelector:
79
116
 
80
117
  return root
81
118
 
82
- def _scan_directory(self, dir_path: str, parent_node: FileNode):
119
+ def _deep_scan_directory_and_calc_size(self, dir_path: str, parent_node: FileNode):
83
120
  """Recursively scan directory and build tree"""
84
121
  abs_dir_path = os.path.abspath(dir_path)
85
122
 
@@ -138,7 +175,7 @@ class TreeSelector:
138
175
  result.append((node, level))
139
176
  if node.is_dir and node.expanded:
140
177
  if not node.children:
141
- self._scan_directory(node.path, node)
178
+ self._deep_scan_directory_and_calc_size(node.path, node)
142
179
  for child in node.children:
143
180
  result.extend(self._flatten_tree(child, level + 1))
144
181
 
@@ -146,6 +183,8 @@ class TreeSelector:
146
183
 
147
184
  def _build_display_tree(self) -> Tree:
148
185
  """Build Rich tree for display with viewport"""
186
+ self._metrics_cache = {} # Clear cache for each new render
187
+
149
188
  # Get terminal size
150
189
  _, term_height = shutil.get_terminal_size()
151
190
 
@@ -177,10 +216,9 @@ class TreeSelector:
177
216
  if self.viewport_offset > 0:
178
217
  tree_title += f" ↑ ({self.viewport_offset} more)"
179
218
 
180
- tree = Tree(tree_title, guide_style="dim")
219
+ tree = Tree(tree_title)
181
220
 
182
221
  # Build tree structure - only for visible portion
183
- node_map = {}
184
222
  viewport_end = min(len(flat_tree), self.viewport_offset + available_height)
185
223
 
186
224
  # Track what level each visible item is at for proper tree structure
@@ -193,30 +231,35 @@ class TreeSelector:
193
231
  is_current = i == self.current_index
194
232
  style = "bold cyan" if is_current else ""
195
233
 
234
+ label = Text()
235
+
196
236
  if node.is_dir:
197
237
  icon = "📂" if node.expanded else "📁"
198
- size_str = f" ({len(node.children)} items)" if node.children else ""
199
- else:
238
+ total_size, selected_size = self._calculate_directory_metrics(node)
239
+ if total_size > 0:
240
+ size_str = f" ({get_human_readable_size(selected_size)} / {get_human_readable_size(total_size)})"
241
+ else:
242
+ size_str = "" # Don't show size for empty dirs
243
+
244
+ # Omit the selection circle for directories
245
+ label.append(f"{icon} {node.name}{size_str}", style=style)
246
+
247
+ else: # It's a file
200
248
  icon = "📄"
201
249
  size_str = f" ({get_human_readable_size(node.size)})"
202
250
 
203
- # Selection indicator
204
- abs_path = os.path.abspath(node.path)
205
- if abs_path in self.selected_files:
206
- is_snippet = self.selected_files[abs_path][0]
207
- if is_snippet:
208
- selection = "" # Half-selected (snippet)
251
+ # File selection indicator
252
+ abs_path = os.path.abspath(node.path)
253
+ if abs_path in self.selected_files:
254
+ is_snippet, _ = self.selected_files[abs_path]
255
+ selection = "◐" if is_snippet else "●"
256
+ style = "green " + style
209
257
  else:
210
- selection = "" # Fully selected
211
- style = "green " + style
212
- else:
213
- selection = "○"
258
+ selection = ""
214
259
 
215
- # Build label
216
- label = Text()
217
- label.append(f"{selection} ", style="dim")
218
- label.append(f"{icon} {node.name}{size_str}", style=style)
219
-
260
+ label.append(f"{selection} ", style="dim")
261
+ label.append(f"{icon} {node.name}{size_str}", style=style)
262
+
220
263
  # Add to tree at correct level
221
264
  if level == 0:
222
265
  tree_node = tree.add(label)
@@ -230,10 +273,17 @@ class TreeSelector:
230
273
  level_stacks[level] = tree_node
231
274
  else:
232
275
  # Fallback - add to root with indentation indicator
233
- indent_label = Text()
234
- indent_label.append(" " * level + f"{selection} ", style="dim")
235
- indent_label.append(f"{icon} {node.name}{size_str}", style=style)
236
- tree_node = tree.add(indent_label)
276
+ indent_text = " " * level
277
+ if not node.is_dir:
278
+ # Re-add file selection marker for indented fallback
279
+ selection_char = "○"
280
+ if node.path in self.selected_files:
281
+ selection_char = "◐" if self.selected_files[node.path][0] else "●"
282
+ indent_text += f"{selection_char} "
283
+
284
+ # Create a new label with proper indentation for this edge case
285
+ fallback_label_text = f"{indent_text}{label.plain}"
286
+ tree_node = tree.add(Text(fallback_label_text, style=style))
237
287
  level_stacks[level] = tree_node
238
288
 
239
289
  # Add scroll indicator at bottom if needed
@@ -344,7 +394,7 @@ q: Quit and finalize"""
344
394
 
345
395
  # Ensure children are loaded
346
396
  if not node.children:
347
- self._scan_directory(node.path, node)
397
+ self._deep_scan_directory_and_calc_size(node.path, node)
348
398
 
349
399
  # Collect all files recursively
350
400
  all_files = []
@@ -353,7 +403,7 @@ q: Quit and finalize"""
353
403
  if n.is_dir:
354
404
  # CRITICAL FIX: Ensure sub-directory children are loaded before recursing
355
405
  if not n.children:
356
- self._scan_directory(n.path, n)
406
+ self._deep_scan_directory_and_calc_size(n.path, n)
357
407
  for child in n.children:
358
408
  collect_files(child)
359
409
  else:
@@ -479,7 +529,7 @@ q: Quit and finalize"""
479
529
  node.expanded = True
480
530
  # Ensure children are loaded
481
531
  if not node.children:
482
- self._scan_directory(node.path, node)
532
+ self._deep_scan_directory_and_calc_size(node.path, node)
483
533
  found = True
484
534
  break
485
535
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -4,10 +4,10 @@ kopipasta/file.py,sha256=tPCLNvSXHzIvAXLB6fgJswLpCFce7T1QnbNdSWHbIso,4829
4
4
  kopipasta/import_parser.py,sha256=yLzkMlQm2avKjfqcpMY0PxbA_2ihV9gSYJplreWIPEQ,12424
5
5
  kopipasta/main.py,sha256=YzzEnDph1v3NojHYIjvD3Z4blnIEX5zqj9W6gNLlA7E,50058
6
6
  kopipasta/prompt.py,sha256=fOCuJVTLUfR0fjKf5qIlnl_3pNsKNKsvt3C8f4tsmxk,6889
7
- kopipasta/tree_selector.py,sha256=dQ2WI4fpAl5BnxpsIMt4n4eS2kBJNNucri8GHCzQM1Y,28246
8
- kopipasta-0.34.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
9
- kopipasta-0.34.0.dist-info/METADATA,sha256=8bHYOP8yyfC4lzcj2Ckca4AgJxwdvWZeEaBYyw96EiY,4894
10
- kopipasta-0.34.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
11
- kopipasta-0.34.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
12
- kopipasta-0.34.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
13
- kopipasta-0.34.0.dist-info/RECORD,,
7
+ kopipasta/tree_selector.py,sha256=lK56mEspCqMgL0YLDmTQWHSXrsmrxScdPK5iGceHVHo,30580
8
+ kopipasta-0.35.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
9
+ kopipasta-0.35.0.dist-info/METADATA,sha256=8q5DXnATeJOEAUqBWIDlGDiLIYslg1NTYyvwujo0xto,4894
10
+ kopipasta-0.35.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
11
+ kopipasta-0.35.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
12
+ kopipasta-0.35.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
13
+ kopipasta-0.35.0.dist-info/RECORD,,