-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCodeVault.py
More file actions
409 lines (361 loc) · 15.6 KB
/
CodeVault.py
File metadata and controls
409 lines (361 loc) · 15.6 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
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
try:
import pathspec # pip install pathspec
except ImportError:
raise ImportError("Please install the 'pathspec' module: pip install pathspec")
# ---------------------------
# IGNORE & EXTRACTION FUNCTIONS
# ---------------------------
def load_gitignore_patterns(root_folder):
"""
Recursively find all .gitignore files in root_folder, read their patterns,
and return a list of patterns adjusted to be relative to the project root.
"""
all_patterns = []
for dirpath, _, files in os.walk(root_folder):
if ".gitignore" in files:
gitignore_path = os.path.join(dirpath, ".gitignore")
try:
with open(gitignore_path, "r", encoding="utf-8") as f:
lines = f.readlines()
except Exception as e:
print(f"Error reading {gitignore_path}: {e}")
continue
rel_prefix = os.path.relpath(dirpath, root_folder)
if rel_prefix == ".":
rel_prefix = ""
for line in lines:
line = line.strip()
if not line or line.startswith("#"):
continue # skip comments/blank lines
if line.startswith("/"):
line = line[1:]
full_pattern = os.path.join(rel_prefix, line) if rel_prefix else line
all_patterns.append(full_pattern)
return all_patterns
def load_ignore_spec(root_folder):
"""
Load all .gitignore patterns from root_folder and compile a pathspec.
"""
patterns = load_gitignore_patterns(root_folder)
if patterns:
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
return None
def should_ignore(path, ignore_spec):
"""
Given a path (using forward slashes) relative to the project root and an ignore_spec,
return True if the path should be ignored.
"""
if ignore_spec is None:
return False
norm_path = path.replace(os.sep, "/")
return ignore_spec.match_file(norm_path)
# ---------------------------
# USER EXCLUSION HELPERS
# ---------------------------
def is_user_excluded(rel_path, exclusions):
"""
Return True if rel_path is either exactly in exclusions or is within an excluded folder.
"""
for excl in exclusions:
if rel_path == excl or rel_path.startswith(excl + os.sep):
return True
return False
def filter_file_contents(file_contents, root_folder, user_exclusions):
"""
Given a dictionary mapping absolute file paths to content,
filter out any file whose relative path (from root_folder) is in user_exclusions.
"""
filtered = {}
for abs_path, content in file_contents.items():
rel_path = os.path.relpath(abs_path, root_folder)
if is_user_excluded(rel_path, user_exclusions):
continue
filtered[abs_path] = content
return filtered
# ---------------------------
# TREE & CONTENT EXTRACTION
# ---------------------------
def generate_tree_string(folder_path, ignore_spec=None):
"""
Generate a string representing the folder tree structure,
ignoring paths that match the ignore_spec as well as any hidden files or directories.
This is used for the non‑interactive display.
"""
tree_lines = []
folder_path = os.path.abspath(folder_path)
for root, dirs, files in os.walk(folder_path):
rel_root = os.path.relpath(root, folder_path)
if rel_root == ".":
rel_root = ""
level = rel_root.count(os.sep) if rel_root else 0
indent = " " * 4 * level
current_dir = os.path.basename(root) if rel_root else os.path.basename(folder_path)
if rel_root and should_ignore(rel_root, ignore_spec):
dirs[:] = [] # prune subdirectories
continue
tree_lines.append(f"{indent}{current_dir}/")
for d in dirs[:]:
if d.startswith("."):
dirs.remove(d)
continue
d_rel = os.path.join(rel_root, d) if rel_root else d
if should_ignore(d_rel, ignore_spec):
dirs.remove(d)
for file in files:
if file.startswith("."):
continue
file_rel = os.path.join(rel_root, file) if rel_root else file
if should_ignore(file_rel, ignore_spec):
continue
tree_lines.append(f"{indent} {file}")
return "\n".join(tree_lines)
def extract_file_contents(folder_path, ignore_spec=None):
"""
Walk through the folder and extract the contents of every file that isn't ignored.
Returns a dictionary mapping absolute file paths to their content.
"""
file_data = {}
folder_path = os.path.abspath(folder_path)
for root, dirs, files in os.walk(folder_path):
rel_root = os.path.relpath(root, folder_path)
if rel_root == ".":
rel_root = ""
for d in dirs[:]:
if d.startswith("."):
dirs.remove(d)
continue
d_rel = os.path.join(rel_root, d) if rel_root else d
if should_ignore(d_rel, ignore_spec):
dirs.remove(d)
for file in files:
if file.startswith("."):
continue
file_rel = os.path.join(rel_root, file) if rel_root else file
if should_ignore(file_rel, ignore_spec):
continue
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
file_data[file_path] = content
except Exception as e:
file_data[file_path] = f"Error reading file: {e}"
return file_data
# ---------------------------
# EXCLUSION MANAGER (ADVANCED)
# ---------------------------
def get_expanded_nodes(tv, parent=""):
"""Recursively collect the relative paths of all expanded nodes in the treeview."""
expanded = set()
for child in tv.get_children(parent):
if tv.item(child, "open"):
rel_path = tv.item(child, "values")[0]
expanded.add(rel_path)
expanded |= get_expanded_nodes(tv, child)
return expanded
def refresh_available_view(avail_tree, root_folder, ignore_spec, exclusions):
"""
Clear and repopulate the available items treeview with items not in exclusions.
Preserve the expansion state of already expanded nodes.
"""
expanded_set = get_expanded_nodes(avail_tree)
avail_tree.delete(*avail_tree.get_children())
def add_nodes(parent, folder):
try:
entries = sorted(os.listdir(folder))
except PermissionError:
return # skip inaccessible folders
for entry in entries:
if entry.startswith("."):
continue # skip hidden items
full_path = os.path.join(folder, entry)
rel_path = os.path.relpath(full_path, root_folder)
if should_ignore(rel_path, ignore_spec) or is_user_excluded(rel_path, exclusions):
continue
node = avail_tree.insert(parent, "end", text=entry, values=(rel_path,))
# If this node was expanded before, open it now.
if rel_path in expanded_set:
avail_tree.item(node, open=True)
if os.path.isdir(full_path):
add_nodes(node, full_path)
add_nodes("", root_folder)
def refresh_excluded_listbox(lb, exclusions):
"""Clear and repopulate the excluded items listbox with current exclusions."""
lb.delete(0, tk.END)
for item in sorted(exclusions):
lb.insert(tk.END, item)
def open_exclusion_manager(root_folder, ignore_spec, current_exclusions, update_callback):
"""
Open an improved exclusion manager window that shows two panels:
- Left: Available Items (treeview) for items not yet excluded.
- Right: Excluded Items (listbox) for items that have been excluded.
Users can add items to exclusions (or remove them) and then apply or cancel their changes.
"""
win = tk.Toplevel()
win.title("Manage Exclusions")
win.geometry("600x500")
win.resizable(False, False)
# Main frame with padding.
main_frame = ttk.Frame(win, padding=10)
main_frame.pack(fill="both", expand=True)
# Two labeled frames for Available and Excluded items.
avail_frame = ttk.LabelFrame(main_frame, text="Available Items", padding=5)
avail_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
excl_frame = ttk.LabelFrame(main_frame, text="Excluded Items", padding=5)
excl_frame.grid(row=0, column=2, sticky="nsew", padx=(5, 0))
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=0)
main_frame.columnconfigure(2, weight=1)
main_frame.rowconfigure(0, weight=1)
# Available items treeview.
avail_tree = ttk.Treeview(avail_frame, columns=("rel_path",), show="tree")
avail_tree.heading("#0", text="Name")
avail_tree.column("rel_path", width=0, stretch=False)
avail_tree.pack(fill="both", expand=True)
avail_scroll = ttk.Scrollbar(avail_frame, orient="vertical", command=avail_tree.yview)
avail_tree.configure(yscrollcommand=avail_scroll.set)
avail_scroll.pack(side="right", fill="y")
# Excluded items listbox.
excl_lb = tk.Listbox(excl_frame, selectmode="extended")
excl_lb.pack(fill="both", expand=True)
excl_scroll = ttk.Scrollbar(excl_frame, orient="vertical", command=excl_lb.yview)
excl_lb.configure(yscrollcommand=excl_scroll.set)
excl_scroll.pack(side="right", fill="y")
# Middle frame for Add/Remove buttons.
mid_frame = ttk.Frame(main_frame)
mid_frame.grid(row=0, column=1, sticky="ns")
btn_add = ttk.Button(mid_frame, text="Add »")
btn_add.pack(pady=(50,10))
btn_remove = ttk.Button(mid_frame, text="« Remove")
btn_remove.pack(pady=10)
# Bottom frame for Apply/Cancel.
bot_frame = ttk.Frame(win, padding=10)
bot_frame.pack(fill="x")
btn_apply = ttk.Button(bot_frame, text="Apply", width=10)
btn_apply.pack(side="right", padx=5)
btn_cancel = ttk.Button(bot_frame, text="Cancel", width=10, command=win.destroy)
btn_cancel.pack(side="right", padx=5)
# Initialize current exclusions locally.
exclusions = set(current_exclusions)
# Populate the two panels.
refresh_available_view(avail_tree, os.path.abspath(root_folder), ignore_spec, exclusions)
refresh_excluded_listbox(excl_lb, exclusions)
def add_selected():
"""Add selected items from available treeview to exclusions."""
selected = avail_tree.selection()
for item in selected:
rel_path = avail_tree.item(item, "values")[0]
exclusions.add(rel_path)
refresh_available_view(avail_tree, os.path.abspath(root_folder), ignore_spec, exclusions)
refresh_excluded_listbox(excl_lb, exclusions)
def remove_selected():
"""Remove selected items from the exclusions listbox."""
selected_indices = list(excl_lb.curselection())
for index in reversed(selected_indices):
rel_path = excl_lb.get(index)
if rel_path in exclusions:
exclusions.remove(rel_path)
refresh_available_view(avail_tree, os.path.abspath(root_folder), ignore_spec, exclusions)
refresh_excluded_listbox(excl_lb, exclusions)
btn_add.config(command=add_selected)
btn_remove.config(command=remove_selected)
def apply_exclusions():
update_callback(exclusions)
win.destroy()
btn_apply.config(command=apply_exclusions)
# ---------------------------
# SAVE COMBINED FILE
# ---------------------------
def save_combined_file(file_contents):
"""
Prompt for a destination file and write out all file contents
(with headers indicating the file paths) to that file.
"""
dest_file = filedialog.asksaveasfilename(
title="Save Combined File",
defaultextension=".txt",
filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]
)
if not dest_file:
return
try:
with open(dest_file, "w", encoding="utf-8") as out:
for file_path, content in file_contents.items():
header = f"\n{'='*80}\n{file_path}\n{'='*80}\n"
out.write(header)
out.write(content + "\n")
messagebox.showinfo("Success", f"Combined file saved to: {dest_file}")
except Exception as e:
messagebox.showerror("Error", f"Error writing to file: {e}")
# ---------------------------
# OUTPUT WINDOW & UPDATE CALLBACK
# ---------------------------
def show_output(folder_selected):
"""
Create the output window that displays the folder tree and file contents.
Also provides buttons to manage exclusions and save the combined file.
"""
folder_selected = os.path.abspath(folder_selected)
ignore_spec = load_ignore_spec(folder_selected)
file_contents_all = extract_file_contents(folder_selected, ignore_spec)
tree_map = generate_tree_string(folder_selected, ignore_spec)
user_exclusions = set()
output_win = tk.Toplevel()
output_win.title("Project Overview")
output_win.geometry("800x600")
notebook = ttk.Notebook(output_win)
notebook.pack(fill="both", expand=True)
# Tab 1: Folder Tree
frame_tree = ttk.Frame(notebook)
notebook.add(frame_tree, text="Folder Tree")
tree_text = scrolledtext.ScrolledText(frame_tree, wrap="none", font=("Courier", 10))
tree_text.pack(fill="both", expand=True)
tree_text.insert(tk.END, tree_map)
tree_text.config(state=tk.DISABLED)
# Tab 2: File Contents
frame_contents = ttk.Frame(notebook)
notebook.add(frame_contents, text="File Contents")
contents_text = scrolledtext.ScrolledText(frame_contents, wrap="none", font=("Courier", 10))
contents_text.pack(fill="both", expand=True)
def update_file_contents(new_exclusions):
nonlocal user_exclusions
user_exclusions = new_exclusions
filtered_contents = filter_file_contents(file_contents_all, folder_selected, user_exclusions)
contents_text.config(state=tk.NORMAL)
contents_text.delete("1.0", tk.END)
for file_path, content in filtered_contents.items():
header = f"\n{'='*80}\n{file_path}\n{'='*80}\n"
contents_text.insert(tk.END, header)
contents_text.insert(tk.END, content + "\n")
contents_text.config(state=tk.DISABLED)
update_file_contents(user_exclusions)
btn_manage = ttk.Button(frame_contents, text="Manage Exclusions",
command=lambda: open_exclusion_manager(folder_selected, ignore_spec, user_exclusions, update_file_contents))
btn_manage.pack(pady=5)
btn_save = ttk.Button(frame_contents, text="Save All to File",
command=lambda: save_combined_file(filter_file_contents(file_contents_all, folder_selected, user_exclusions)))
btn_save.pack(pady=5)
# ---------------------------
# MAIN WINDOW
# ---------------------------
def select_folder():
folder_selected = filedialog.askdirectory(title="Select your project folder")
if not folder_selected:
messagebox.showinfo("No Selection", "No folder was selected. Please try again.")
else:
show_output(folder_selected)
def main():
root = tk.Tk()
root.title("Project Extractor Tool")
root.geometry("400x150")
label = ttk.Label(root, text="Select your project folder to extract its structure and file contents:")
label.pack(pady=20, padx=20)
select_button = ttk.Button(root, text="Select Folder", command=select_folder)
select_button.pack()
root.eval('tk::PlaceWindow . center')
root.mainloop()
if __name__ == "__main__":
main()