kopipasta 0.33.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,11 +183,13 @@ 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
 
152
191
  # Reserve space for header, help panel, and status
153
- reserved_space = 17
192
+ reserved_space = 12
154
193
  available_height = term_height - reserved_space
155
194
  available_height = max(5, available_height) # Minimum height
156
195
 
@@ -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
@@ -245,14 +295,9 @@ class TreeSelector:
245
295
 
246
296
  def _show_help(self) -> Panel:
247
297
  """Create help panel"""
248
- help_text = """[bold]Navigation:[/bold]
249
- ↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse
250
-
251
- [bold]Selection:[/bold]
252
- Space: Toggle file/dir a: Add all in dir s: Snippet mode
253
-
254
- [bold]Actions:[/bold]
255
- r: Reuse last selection g: Grep in directory d: Show dependencies
298
+ help_text = """[bold]Navigation:[/bold] ↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse
299
+ [bold]Selection:[/bold] Space: Toggle file/dir a: Add all in dir s: Snippet mode
300
+ [bold]Actions:[/bold] r: Reuse last selection g: Grep in directory d: Show dependencies
256
301
  q: Quit and finalize"""
257
302
 
258
303
  return Panel(help_text, title="Keyboard Controls", border_style="dim", expand=False)
@@ -349,7 +394,7 @@ q: Quit and finalize"""
349
394
 
350
395
  # Ensure children are loaded
351
396
  if not node.children:
352
- self._scan_directory(node.path, node)
397
+ self._deep_scan_directory_and_calc_size(node.path, node)
353
398
 
354
399
  # Collect all files recursively
355
400
  all_files = []
@@ -358,7 +403,7 @@ q: Quit and finalize"""
358
403
  if n.is_dir:
359
404
  # CRITICAL FIX: Ensure sub-directory children are loaded before recursing
360
405
  if not n.children:
361
- self._scan_directory(n.path, n)
406
+ self._deep_scan_directory_and_calc_size(n.path, n)
362
407
  for child in n.children:
363
408
  collect_files(child)
364
409
  else:
@@ -484,7 +529,7 @@ q: Quit and finalize"""
484
529
  node.expanded = True
485
530
  # Ensure children are loaded
486
531
  if not node.children:
487
- self._scan_directory(node.path, node)
532
+ self._deep_scan_directory_and_calc_size(node.path, node)
488
533
  found = True
489
534
  break
490
535
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.33.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=VIBmPq5cTHU-xUJuXV3K58nn1zS-1X079WAFzfS2vCA,28247
8
- kopipasta-0.33.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
9
- kopipasta-0.33.0.dist-info/METADATA,sha256=Fs44BJq3C5tqpIdzccr6APhyx97UU0JHjlcTb1uqzuI,4894
10
- kopipasta-0.33.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
11
- kopipasta-0.33.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
12
- kopipasta-0.33.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
13
- kopipasta-0.33.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,,