brain-cleaner 1.2.0
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.
- package/README.es.md +81 -0
- package/README.md +97 -0
- package/app.py +709 -0
- package/assets/icon.png +0 -0
- package/assets/icon_original.png +0 -0
- package/assets/icon_ui.png +0 -0
- package/bin/brain-cleaner.js +48 -0
- package/console/README.md +43 -0
- package/console/brain_cleaner_cli.py +379 -0
- package/package.json +29 -0
- package/scanner.py +146 -0
package/app.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# macOS Compatibility fixes (handled by system environment)
|
|
5
|
+
import customtkinter as ctk
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import subprocess
|
|
9
|
+
from scanner import BrainScanner
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
ctk.set_appearance_mode("Dark")
|
|
14
|
+
ctk.set_default_color_theme("blue")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BrainCleanerApp(ctk.CTk):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__()
|
|
20
|
+
|
|
21
|
+
self.title("Brain Cleaner - AI Residue Removal")
|
|
22
|
+
self.geometry("1100x720")
|
|
23
|
+
self.scanner = BrainScanner()
|
|
24
|
+
self.interrupt_event = threading.Event()
|
|
25
|
+
self.residue_rows = [] # [(wrapper, cat, var, path, children_frame, child_rows)]
|
|
26
|
+
self.filter_buttons = {}
|
|
27
|
+
self.active_filter = "All"
|
|
28
|
+
self.current_scan_path = str(Path.home())
|
|
29
|
+
self.current_loc_type = "Home"
|
|
30
|
+
|
|
31
|
+
# Load Icon
|
|
32
|
+
assets_dir = os.path.join(os.path.dirname(__file__), "assets")
|
|
33
|
+
icon_ui_path = os.path.join(assets_dir, "icon_ui.png")
|
|
34
|
+
self.logo_image = None
|
|
35
|
+
if os.path.exists(icon_ui_path):
|
|
36
|
+
try:
|
|
37
|
+
img = Image.open(icon_ui_path)
|
|
38
|
+
self.logo_image = ctk.CTkImage(light_image=img, dark_image=img, size=(42, 42))
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
# Category definitions
|
|
43
|
+
self.AI_CATS = ["Gemini", "Claude", "IDE Agents", "Other Tools"]
|
|
44
|
+
self.NPM_CATS = ["Node Modules"]
|
|
45
|
+
|
|
46
|
+
# Info slider data
|
|
47
|
+
self.info_states = [
|
|
48
|
+
{
|
|
49
|
+
"title": "Delete Consequences",
|
|
50
|
+
"text": "You will free up space, but might lose local chat histories and plugin configurations.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"title": "Usage Tips",
|
|
54
|
+
"text": "Scan weekly to keep your system optimized. Use 'Custom Folder' for specific project cleanups.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"title": "Pro Optimization",
|
|
58
|
+
"text": "For a full cleanup, close your IDE (Cursor, VSCode) before cleaning temporary files.",
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
self.current_info_index = 0
|
|
62
|
+
|
|
63
|
+
# ── Grid Layout ──────────────────────────────────────────
|
|
64
|
+
self.grid_columnconfigure(1, weight=1)
|
|
65
|
+
self.grid_rowconfigure(0, weight=1)
|
|
66
|
+
|
|
67
|
+
# ── Sidebar ───────────────────────────────────────────────
|
|
68
|
+
self.sidebar = ctk.CTkFrame(self, width=210, corner_radius=0)
|
|
69
|
+
self.sidebar.grid(row=0, column=0, rowspan=4, sticky="nsew")
|
|
70
|
+
self.sidebar.grid_rowconfigure(9, weight=1)
|
|
71
|
+
self.sidebar.grid_columnconfigure(0, weight=1)
|
|
72
|
+
|
|
73
|
+
title_f = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
|
74
|
+
title_f.grid(row=0, column=0, padx=20, pady=(15, 15), sticky="ew")
|
|
75
|
+
|
|
76
|
+
if self.logo_image:
|
|
77
|
+
ctk.CTkLabel(title_f, text="", image=self.logo_image).pack(pady=(0, 8))
|
|
78
|
+
|
|
79
|
+
ctk.CTkLabel(title_f, text="Brain Cleaner",
|
|
80
|
+
font=ctk.CTkFont(size=19, weight="bold")
|
|
81
|
+
).pack(pady=(0, 2))
|
|
82
|
+
ctk.CTkLabel(title_f, text="v1.1.0",
|
|
83
|
+
font=ctk.CTkFont(size=11, slant="italic"), text_color="#a1a1a1"
|
|
84
|
+
).pack()
|
|
85
|
+
|
|
86
|
+
# Location
|
|
87
|
+
ctk.CTkLabel(self.sidebar, text="Scan Scope:", anchor="w",
|
|
88
|
+
font=ctk.CTkFont(size=12, weight="bold")
|
|
89
|
+
).grid(row=1, column=0, padx=20, pady=(0, 5), sticky="ew")
|
|
90
|
+
|
|
91
|
+
self.home_btn = ctk.CTkButton(self.sidebar, text="Home",
|
|
92
|
+
command=lambda: self.set_location("Home"),
|
|
93
|
+
fg_color="transparent", border_width=1, anchor="w")
|
|
94
|
+
self.home_btn.grid(row=2, column=0, padx=20, pady=3, sticky="ew")
|
|
95
|
+
|
|
96
|
+
self.system_btn = ctk.CTkButton(self.sidebar, text="Full System",
|
|
97
|
+
command=lambda: self.set_location("System"),
|
|
98
|
+
fg_color="transparent", border_width=1, anchor="w")
|
|
99
|
+
self.system_btn.grid(row=3, column=0, padx=20, pady=3, sticky="ew")
|
|
100
|
+
|
|
101
|
+
self.custom_folder_btn = ctk.CTkButton(self.sidebar, text="Custom Folder",
|
|
102
|
+
command=self.select_custom_folder,
|
|
103
|
+
fg_color="transparent", border_width=1, anchor="w")
|
|
104
|
+
self.custom_folder_btn.grid(row=4, column=0, padx=20, pady=3, sticky="ew")
|
|
105
|
+
|
|
106
|
+
self._highlight_loc_btn()
|
|
107
|
+
|
|
108
|
+
# Scan Mode
|
|
109
|
+
ctk.CTkLabel(self.sidebar, text="Scan Mode:", anchor="w",
|
|
110
|
+
font=ctk.CTkFont(size=12, weight="bold")
|
|
111
|
+
).grid(row=5, column=0, padx=20, pady=(14, 4), sticky="ew")
|
|
112
|
+
|
|
113
|
+
self.scan_mode_var = ctk.StringVar(value="ai")
|
|
114
|
+
|
|
115
|
+
ctk.CTkRadioButton(self.sidebar, text="AI Tools",
|
|
116
|
+
variable=self.scan_mode_var, value="ai",
|
|
117
|
+
font=ctk.CTkFont(size=12), border_color="#1f538d",
|
|
118
|
+
hover_color="#1f538d", fg_color="#1f538d",
|
|
119
|
+
command=self._update_mode_ui
|
|
120
|
+
).grid(row=6, column=0, padx=24, pady=2, sticky="w")
|
|
121
|
+
|
|
122
|
+
ctk.CTkRadioButton(self.sidebar, text="NPM Modules",
|
|
123
|
+
variable=self.scan_mode_var, value="npm",
|
|
124
|
+
font=ctk.CTkFont(size=12), border_color="#2e7d32",
|
|
125
|
+
hover_color="#2e7d32", fg_color="#2e7d32",
|
|
126
|
+
command=self._update_mode_ui
|
|
127
|
+
).grid(row=7, column=0, padx=24, pady=(2, 4), sticky="w")
|
|
128
|
+
|
|
129
|
+
# ── Bottom Sidebar Controls ───────────────────────────────
|
|
130
|
+
bottom = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
|
131
|
+
bottom.grid(row=10, column=0, padx=20, pady=20, sticky="ew")
|
|
132
|
+
bottom.grid_columnconfigure(0, weight=1)
|
|
133
|
+
|
|
134
|
+
self.run_scan_button = ctk.CTkButton(
|
|
135
|
+
bottom, text="START SCAN",
|
|
136
|
+
command=lambda: self.start_scan(self.current_scan_path),
|
|
137
|
+
fg_color="#1f538d", hover_color="#14375e",
|
|
138
|
+
height=130, corner_radius=18,
|
|
139
|
+
font=ctk.CTkFont(size=15, weight="bold"))
|
|
140
|
+
self.run_scan_button.grid(row=0, column=0, pady=(0, 15), sticky="ew")
|
|
141
|
+
|
|
142
|
+
self.stop_scan_button = ctk.CTkButton(
|
|
143
|
+
bottom, text="STOP",
|
|
144
|
+
command=self.stop_scan, fg_color="#d32f2f", hover_color="#b71c1c",
|
|
145
|
+
height=130, corner_radius=18,
|
|
146
|
+
font=ctk.CTkFont(size=15, weight="bold"))
|
|
147
|
+
self.stop_scan_button.grid(row=0, column=0, pady=(0, 15), sticky="ew")
|
|
148
|
+
self.stop_scan_button.grid_remove()
|
|
149
|
+
|
|
150
|
+
self.clean_selected_button = ctk.CTkButton(
|
|
151
|
+
bottom, text="Clean Selected",
|
|
152
|
+
command=self.clean_selected, state="disabled",
|
|
153
|
+
fg_color="#2b71b1", hover_color="#1a4d7d",
|
|
154
|
+
height=38, font=ctk.CTkFont(weight="bold"))
|
|
155
|
+
self.clean_selected_button.grid(row=1, column=0, pady=4, sticky="ew")
|
|
156
|
+
|
|
157
|
+
self.clean_all_button = ctk.CTkButton(
|
|
158
|
+
bottom, text="Clean All (Visible)",
|
|
159
|
+
command=self.clean_all, state="disabled",
|
|
160
|
+
fg_color="transparent", border_width=1,
|
|
161
|
+
text_color=("#d32f2f", "#ff6666"), height=30)
|
|
162
|
+
self.clean_all_button.grid(row=2, column=0, pady=(4, 12), sticky="ew")
|
|
163
|
+
|
|
164
|
+
# Appearance
|
|
165
|
+
ctk.CTkLabel(self.sidebar, text="Appearance:", anchor="w",
|
|
166
|
+
font=ctk.CTkFont(size=10)
|
|
167
|
+
).grid(row=11, column=0, padx=20, pady=(0, 2), sticky="ew")
|
|
168
|
+
app_menu = ctk.CTkOptionMenu(self.sidebar, values=["Dark", "Light", "System"],
|
|
169
|
+
command=lambda m: ctk.set_appearance_mode(m),
|
|
170
|
+
height=24, font=ctk.CTkFont(size=10))
|
|
171
|
+
app_menu.grid(row=12, column=0, padx=20, pady=(0, 5), sticky="ew")
|
|
172
|
+
app_menu.set("Dark")
|
|
173
|
+
|
|
174
|
+
self.show_logs_var = ctk.BooleanVar(value=False)
|
|
175
|
+
ctk.CTkSwitch(self.sidebar, text="Show Activity Logs",
|
|
176
|
+
variable=self.show_logs_var, command=self.toggle_logs,
|
|
177
|
+
font=ctk.CTkFont(size=10)
|
|
178
|
+
).grid(row=13, column=0, padx=20, pady=10, sticky="ew")
|
|
179
|
+
|
|
180
|
+
# ── Main Area ─────────────────────────────────────────────
|
|
181
|
+
main = ctk.CTkFrame(self, fg_color="transparent")
|
|
182
|
+
main.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
|
|
183
|
+
main.grid_columnconfigure(0, weight=1)
|
|
184
|
+
main.grid_rowconfigure(4, weight=1)
|
|
185
|
+
self.main = main
|
|
186
|
+
|
|
187
|
+
# Info Slider
|
|
188
|
+
self.info_bubble = ctk.CTkFrame(main, corner_radius=14, border_width=1)
|
|
189
|
+
self.info_bubble.grid(row=0, column=0, padx=10, pady=(5, 5), sticky="ew")
|
|
190
|
+
self.info_bubble.grid_columnconfigure(1, weight=1)
|
|
191
|
+
|
|
192
|
+
ctk.CTkButton(self.info_bubble, text="❮", width=28, fg_color="transparent",
|
|
193
|
+
hover_color=("#c0c0c0", "#3d3d3d"),
|
|
194
|
+
command=lambda: self.navigate_slider(-1)
|
|
195
|
+
).grid(row=0, column=0, rowspan=2, padx=(8, 0))
|
|
196
|
+
|
|
197
|
+
self.info_title = ctk.CTkLabel(self.info_bubble, text=self.info_states[0]["title"],
|
|
198
|
+
font=ctk.CTkFont(size=13, weight="bold"), anchor="w")
|
|
199
|
+
self.info_title.grid(row=0, column=1, pady=(8, 0), sticky="ew", padx=(8, 0))
|
|
200
|
+
|
|
201
|
+
self.info_text = ctk.CTkLabel(self.info_bubble, text=self.info_states[0]["text"],
|
|
202
|
+
font=ctk.CTkFont(size=10), wraplength=480,
|
|
203
|
+
justify="left", anchor="w")
|
|
204
|
+
self.info_text.grid(row=1, column=1, pady=(2, 4), sticky="ew", padx=(8, 0))
|
|
205
|
+
|
|
206
|
+
ctk.CTkButton(self.info_bubble, text="❯", width=28, fg_color="transparent",
|
|
207
|
+
hover_color=("#c0c0c0", "#3d3d3d"),
|
|
208
|
+
command=lambda: self.navigate_slider(1)
|
|
209
|
+
).grid(row=0, column=2, rowspan=2, padx=(0, 8))
|
|
210
|
+
|
|
211
|
+
dots_f = ctk.CTkFrame(self.info_bubble, fg_color="transparent")
|
|
212
|
+
dots_f.grid(row=2, column=1, pady=(0, 6), sticky="w", padx=(8, 0))
|
|
213
|
+
self.dots_labels = []
|
|
214
|
+
for i in range(len(self.info_states)):
|
|
215
|
+
d = ctk.CTkLabel(dots_f, text="●" if i == 0 else "○", font=ctk.CTkFont(size=9))
|
|
216
|
+
d.pack(side="left", padx=2)
|
|
217
|
+
self.dots_labels.append(d)
|
|
218
|
+
|
|
219
|
+
# Filter Bubbles Bar
|
|
220
|
+
filter_bar = ctk.CTkFrame(main, fg_color="transparent")
|
|
221
|
+
filter_bar.grid(row=1, column=0, padx=10, pady=(10, 0), sticky="ew")
|
|
222
|
+
|
|
223
|
+
ctk.CTkLabel(filter_bar, text="Filters:", font=ctk.CTkFont(size=12, weight="bold")
|
|
224
|
+
).pack(side="left", padx=(5, 10))
|
|
225
|
+
|
|
226
|
+
self.bubbles_container = ctk.CTkFrame(filter_bar, fg_color="transparent")
|
|
227
|
+
self.bubbles_container.pack(side="left", fill="x", expand=True)
|
|
228
|
+
|
|
229
|
+
# Select All / None (right side)
|
|
230
|
+
ctk.CTkButton(filter_bar, text="☑ All", width=62, height=26,
|
|
231
|
+
corner_radius=8, border_width=1, border_color="#757575",
|
|
232
|
+
fg_color="transparent", hover_color=("#d0d0d0", "#3a3a3a"),
|
|
233
|
+
text_color=("#333333", "#cccccc"), font=ctk.CTkFont(size=11),
|
|
234
|
+
command=lambda: self.set_all_visible(True)
|
|
235
|
+
).pack(side="right", padx=(4, 2))
|
|
236
|
+
|
|
237
|
+
ctk.CTkButton(filter_bar, text="☐ None", width=68, height=26,
|
|
238
|
+
corner_radius=8, border_width=1, border_color="#757575",
|
|
239
|
+
fg_color="transparent", hover_color=("#d0d0d0", "#3a3a3a"),
|
|
240
|
+
text_color=("#333333", "#cccccc"), font=ctk.CTkFont(size=11),
|
|
241
|
+
command=lambda: self.set_all_visible(False)
|
|
242
|
+
).pack(side="right", padx=(2, 4))
|
|
243
|
+
|
|
244
|
+
# Progress Area
|
|
245
|
+
self.progress_container = ctk.CTkFrame(main, fg_color="transparent")
|
|
246
|
+
self.progress_container.grid(row=2, column=0, padx=10, pady=(8, 0), sticky="ew")
|
|
247
|
+
self.progress_container.grid_columnconfigure(0, weight=1)
|
|
248
|
+
self.progress_container.grid_remove()
|
|
249
|
+
|
|
250
|
+
self.progress_label = ctk.CTkLabel(self.progress_container, text="",
|
|
251
|
+
font=ctk.CTkFont(size=10, slant="italic"))
|
|
252
|
+
self.progress_label.grid(row=0, column=0, pady=(0, 2))
|
|
253
|
+
|
|
254
|
+
self.progress_bar = ctk.CTkProgressBar(self.progress_container, mode="indeterminate",
|
|
255
|
+
height=10, progress_color="#1f538d")
|
|
256
|
+
self.progress_bar.grid(row=1, column=0, padx=10, pady=(0, 5), sticky="ew")
|
|
257
|
+
|
|
258
|
+
# Static Header for Scan Selection
|
|
259
|
+
self.scan_header_frame = ctk.CTkFrame(main, fg_color="#1f538d", corner_radius=8)
|
|
260
|
+
self.scan_header_frame.grid(row=3, column=0, padx=10, pady=(8, 0), sticky="ew")
|
|
261
|
+
|
|
262
|
+
txt_f = ctk.CTkFrame(self.scan_header_frame, fg_color="transparent")
|
|
263
|
+
txt_f.pack(side="left", fill="x", expand=True, padx=12, pady=10)
|
|
264
|
+
|
|
265
|
+
self.scan_header_title = ctk.CTkLabel(txt_f, text="AI Tools Cleanup",
|
|
266
|
+
font=ctk.CTkFont(size=14, weight="bold"),
|
|
267
|
+
text_color="white", anchor="w")
|
|
268
|
+
self.scan_header_title.pack(fill="x")
|
|
269
|
+
self.scan_header_desc = ctk.CTkLabel(txt_f, text="Identify and remove cache, logs, and configs left by AI assistants.",
|
|
270
|
+
font=ctk.CTkFont(size=11), text_color="#e0e0e0", anchor="w")
|
|
271
|
+
self.scan_header_desc.pack(fill="x", pady=(2, 0))
|
|
272
|
+
|
|
273
|
+
self.scan_header_count = ctk.CTkLabel(self.scan_header_frame, text="",
|
|
274
|
+
font=ctk.CTkFont(size=13, weight="bold"), text_color="#ffffff")
|
|
275
|
+
self.scan_header_count.pack(side="right", padx=16, pady=10)
|
|
276
|
+
|
|
277
|
+
# Results Scrollable Frame
|
|
278
|
+
self.results_frame = ctk.CTkScrollableFrame(main)
|
|
279
|
+
self.results_frame.grid(row=4, column=0, padx=10, pady=8, sticky="nsew")
|
|
280
|
+
self.results_frame.grid_columnconfigure(0, weight=1)
|
|
281
|
+
|
|
282
|
+
# Footer
|
|
283
|
+
footer = ctk.CTkFrame(main, fg_color="transparent")
|
|
284
|
+
footer.grid(row=5, column=0, padx=10, pady=(0, 5), sticky="ew")
|
|
285
|
+
|
|
286
|
+
ctk.CTkLabel(footer, text="AI Residue Manager",
|
|
287
|
+
font=ctk.CTkFont(size=14, weight="bold")).pack(side="left", padx=5)
|
|
288
|
+
|
|
289
|
+
self.path_display = ctk.CTkLabel(footer, text=f"Target: {self.current_scan_path}",
|
|
290
|
+
font=ctk.CTkFont(size=10, slant="italic"))
|
|
291
|
+
self.path_display.pack(side="left", padx=15)
|
|
292
|
+
|
|
293
|
+
self.status_label = ctk.CTkLabel(footer, text="Ready to scan",
|
|
294
|
+
font=ctk.CTkFont(size=11))
|
|
295
|
+
self.status_label.pack(side="right", padx=10)
|
|
296
|
+
|
|
297
|
+
# Log Area
|
|
298
|
+
self.log_textbox = ctk.CTkTextbox(main, height=80, font=ctk.CTkFont(size=10))
|
|
299
|
+
self.log_textbox.grid(row=6, column=0, padx=10, pady=(0, 8), sticky="nsew")
|
|
300
|
+
self.log_textbox.insert("0.0", "--- Activity Log ---\n")
|
|
301
|
+
self.log_textbox.configure(state="disabled")
|
|
302
|
+
self.log_textbox.grid_remove()
|
|
303
|
+
|
|
304
|
+
self.create_filter_bubbles(["All"])
|
|
305
|
+
|
|
306
|
+
# Initialize mode UI
|
|
307
|
+
self._update_mode_ui()
|
|
308
|
+
|
|
309
|
+
def _update_mode_ui(self):
|
|
310
|
+
mode = self.scan_mode_var.get()
|
|
311
|
+
if mode == "ai":
|
|
312
|
+
self.scan_header_frame.configure(fg_color="#1f538d")
|
|
313
|
+
self.scan_header_title.configure(text="AI Tools Cleanup")
|
|
314
|
+
self.scan_header_desc.configure(text="Identify and remove cache, logs, and configs left by AI assistants (Gemini, Claude, Cursor...).")
|
|
315
|
+
else:
|
|
316
|
+
self.scan_header_frame.configure(fg_color="#2e7d32")
|
|
317
|
+
self.scan_header_title.configure(text="NPM Modules Cleanup")
|
|
318
|
+
self.scan_header_desc.configure(text="Free up space by removing heavy node_modules directories from web projects.")
|
|
319
|
+
self.scan_header_count.configure(text="")
|
|
320
|
+
|
|
321
|
+
# ── Helpers ───────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
def log(self, text):
|
|
324
|
+
self.log_textbox.configure(state="normal")
|
|
325
|
+
self.log_textbox.insert("end", f"{text}\n")
|
|
326
|
+
self.log_textbox.see("end")
|
|
327
|
+
self.log_textbox.configure(state="disabled")
|
|
328
|
+
|
|
329
|
+
def toggle_logs(self):
|
|
330
|
+
if self.show_logs_var.get():
|
|
331
|
+
self.log_textbox.grid()
|
|
332
|
+
else:
|
|
333
|
+
self.log_textbox.grid_remove()
|
|
334
|
+
|
|
335
|
+
def get_category_color(self, cat):
|
|
336
|
+
return {
|
|
337
|
+
"Gemini": "#1a73e8",
|
|
338
|
+
"Claude": "#d97757",
|
|
339
|
+
"IDE Agents": "#7c4dff",
|
|
340
|
+
"Other Tools": "#546e7a",
|
|
341
|
+
"Node Modules": "#2e7d32"
|
|
342
|
+
}.get(cat, "#757575")
|
|
343
|
+
|
|
344
|
+
# ── Info Slider ───────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
def navigate_slider(self, direction):
|
|
347
|
+
self.current_info_index = (self.current_info_index + direction) % len(self.info_states)
|
|
348
|
+
state = self.info_states[self.current_info_index]
|
|
349
|
+
self.info_bubble.configure(border_width=3)
|
|
350
|
+
self.after(150, lambda: self.info_bubble.configure(border_width=1))
|
|
351
|
+
self.info_title.configure(text=state["title"])
|
|
352
|
+
self.info_text.configure(text=state["text"])
|
|
353
|
+
for i, dot in enumerate(self.dots_labels):
|
|
354
|
+
dot.configure(text="●" if i == self.current_info_index else "○")
|
|
355
|
+
|
|
356
|
+
# ── Location ──────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
def set_location(self, loc_type):
|
|
359
|
+
self.current_loc_type = loc_type
|
|
360
|
+
if loc_type == "Home":
|
|
361
|
+
self.current_scan_path = str(Path.home())
|
|
362
|
+
elif loc_type == "System":
|
|
363
|
+
self.current_scan_path = "/"
|
|
364
|
+
self._highlight_loc_btn()
|
|
365
|
+
self.path_display.configure(text=f"Target: {self.current_scan_path}")
|
|
366
|
+
|
|
367
|
+
def _highlight_loc_btn(self):
|
|
368
|
+
mapping = {"Home": self.home_btn, "System": self.system_btn, "Custom": self.custom_folder_btn}
|
|
369
|
+
for name, btn in mapping.items():
|
|
370
|
+
try:
|
|
371
|
+
if name == self.current_loc_type:
|
|
372
|
+
btn.configure(fg_color="#1f538d", border_width=0)
|
|
373
|
+
else:
|
|
374
|
+
btn.configure(fg_color="transparent", border_width=1)
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
def select_custom_folder(self):
|
|
379
|
+
path = ctk.filedialog.askdirectory()
|
|
380
|
+
if path:
|
|
381
|
+
self.current_scan_path = path
|
|
382
|
+
self.current_loc_type = "Custom"
|
|
383
|
+
self._highlight_loc_btn()
|
|
384
|
+
self.path_display.configure(text=f"Target: {path}")
|
|
385
|
+
self.log(f"Custom path: {path}")
|
|
386
|
+
|
|
387
|
+
# ── Scan ──────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
def stop_scan(self):
|
|
390
|
+
self.interrupt_event.set()
|
|
391
|
+
self.stop_scan_button.configure(state="disabled")
|
|
392
|
+
self.log("Stopping scan...")
|
|
393
|
+
|
|
394
|
+
def start_scan(self, path):
|
|
395
|
+
self.interrupt_event.clear()
|
|
396
|
+
|
|
397
|
+
# Reset results
|
|
398
|
+
for frame, *_ in self.residue_rows:
|
|
399
|
+
frame.destroy()
|
|
400
|
+
self.residue_rows = []
|
|
401
|
+
|
|
402
|
+
# UI state
|
|
403
|
+
self.run_scan_button.grid_remove()
|
|
404
|
+
self.stop_scan_button.grid()
|
|
405
|
+
self.stop_scan_button.configure(state="normal")
|
|
406
|
+
self.clean_selected_button.configure(state="disabled")
|
|
407
|
+
self.clean_all_button.configure(state="disabled")
|
|
408
|
+
|
|
409
|
+
self.progress_label.configure(text=f"Scanning {path} ...")
|
|
410
|
+
self.progress_container.grid()
|
|
411
|
+
self.progress_bar.start()
|
|
412
|
+
self.status_label.configure(text="Scanning...")
|
|
413
|
+
self.create_filter_bubbles(["Scanning..."])
|
|
414
|
+
|
|
415
|
+
self.scanning_active = True
|
|
416
|
+
self.scan_icons = ["-", "\\", "|", "/"]
|
|
417
|
+
self.scan_icon_idx = 0
|
|
418
|
+
self._animate_scan()
|
|
419
|
+
|
|
420
|
+
mode = self.scan_mode_var.get()
|
|
421
|
+
self.log(f"Scan: {path} | mode: {mode}")
|
|
422
|
+
threading.Thread(target=self._run_scan, args=(path, mode), daemon=True).start()
|
|
423
|
+
|
|
424
|
+
def _animate_scan(self):
|
|
425
|
+
if getattr(self, "scanning_active", False):
|
|
426
|
+
icon = self.scan_icons[self.scan_icon_idx % len(self.scan_icons)]
|
|
427
|
+
self.stop_scan_button.configure(text=f"{icon} SCANNING...")
|
|
428
|
+
self.scan_icon_idx += 1
|
|
429
|
+
self.after(400, self._animate_scan)
|
|
430
|
+
|
|
431
|
+
def _run_scan(self, path, mode):
|
|
432
|
+
raw = self.scanner.find_residues(path, self.interrupt_event)
|
|
433
|
+
# Filter by mode
|
|
434
|
+
results = {"All": []}
|
|
435
|
+
if mode == "ai":
|
|
436
|
+
for c in self.AI_CATS:
|
|
437
|
+
results[c] = raw.get(c, [])
|
|
438
|
+
results["All"] += results[c]
|
|
439
|
+
elif mode == "npm":
|
|
440
|
+
for c in self.NPM_CATS:
|
|
441
|
+
results[c] = raw.get(c, [])
|
|
442
|
+
results["All"] += results[c]
|
|
443
|
+
self.scanning_active = False
|
|
444
|
+
self.after(0, lambda: self._finish_scan(results, mode))
|
|
445
|
+
|
|
446
|
+
def _finish_scan(self, results, mode):
|
|
447
|
+
self.progress_bar.stop()
|
|
448
|
+
self.progress_container.grid_remove()
|
|
449
|
+
self.stop_scan_button.grid_remove()
|
|
450
|
+
self.run_scan_button.grid()
|
|
451
|
+
|
|
452
|
+
total = len(results.get("All", []))
|
|
453
|
+
total_bytes = sum(item[2] for item in results.get("All", []))
|
|
454
|
+
total_str = self.scanner.format_size(total_bytes)
|
|
455
|
+
|
|
456
|
+
if total == 0:
|
|
457
|
+
self.status_label.configure(text="No residues found.")
|
|
458
|
+
self.log("Scan complete. Nothing found.")
|
|
459
|
+
self.create_filter_bubbles(["No results"])
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
self.status_label.configure(text=f"Found {total} items — {total_str}")
|
|
463
|
+
self.log(f"Scan complete. {total} items found ({total_str}).")
|
|
464
|
+
|
|
465
|
+
found_cats = [c for c in (self.AI_CATS + self.NPM_CATS) if results.get(c)]
|
|
466
|
+
self.create_filter_bubbles(["All"] + found_cats)
|
|
467
|
+
|
|
468
|
+
count = 0
|
|
469
|
+
if mode == "ai":
|
|
470
|
+
section_cats = [c for c in self.AI_CATS if results.get(c)]
|
|
471
|
+
count = sum(len(results[c]) for c in section_cats)
|
|
472
|
+
for cat in section_cats:
|
|
473
|
+
self._render_rows(results[cat], cat)
|
|
474
|
+
elif mode == "npm":
|
|
475
|
+
section_cats = [c for c in self.NPM_CATS if results.get(c)]
|
|
476
|
+
count = sum(len(results[c]) for c in section_cats)
|
|
477
|
+
for cat in section_cats:
|
|
478
|
+
self._render_rows(results[cat], cat)
|
|
479
|
+
|
|
480
|
+
if count > 0:
|
|
481
|
+
self.scan_header_count.configure(text=f"{count} items")
|
|
482
|
+
|
|
483
|
+
self.clean_selected_button.configure(state="normal")
|
|
484
|
+
self.clean_all_button.configure(state="normal")
|
|
485
|
+
self.active_filter = "All"
|
|
486
|
+
self.update_bubble_selection("All")
|
|
487
|
+
|
|
488
|
+
def _render_rows(self, items, cat):
|
|
489
|
+
"""Render a list of (path, size_str, size_bytes) result items."""
|
|
490
|
+
for path, size_str, _ in items:
|
|
491
|
+
var = ctk.BooleanVar(value=False)
|
|
492
|
+
|
|
493
|
+
wrapper = ctk.CTkFrame(self.results_frame, fg_color="transparent")
|
|
494
|
+
wrapper.pack(fill="x", padx=4, pady=2)
|
|
495
|
+
|
|
496
|
+
row = ctk.CTkFrame(wrapper, fg_color=("#f5f5f5", "#2b2b2b"), corner_radius=8)
|
|
497
|
+
row.pack(fill="x")
|
|
498
|
+
row.grid_columnconfigure(3, weight=1)
|
|
499
|
+
|
|
500
|
+
ctk.CTkCheckBox(row, text="", variable=var, width=24
|
|
501
|
+
).grid(row=0, column=0, padx=(10, 4), pady=6)
|
|
502
|
+
|
|
503
|
+
ctk.CTkLabel(row, text=f" {cat} ",
|
|
504
|
+
fg_color=self.get_category_color(cat),
|
|
505
|
+
text_color="white", corner_radius=5,
|
|
506
|
+
font=ctk.CTkFont(size=9, weight="bold")
|
|
507
|
+
).grid(row=0, column=1, padx=(4, 10), pady=6)
|
|
508
|
+
|
|
509
|
+
size_lbl = ctk.CTkLabel(row, text=size_str, text_color="#FF9500",
|
|
510
|
+
font=ctk.CTkFont(size=11, weight="bold"),
|
|
511
|
+
width=72, anchor="e")
|
|
512
|
+
size_lbl.grid(row=0, column=2, padx=(0, 10), pady=6)
|
|
513
|
+
|
|
514
|
+
path_lbl = ctk.CTkLabel(row, text=path,
|
|
515
|
+
font=ctk.CTkFont(size=11, weight="bold"), anchor="w")
|
|
516
|
+
path_lbl.grid(row=0, column=3, padx=(0, 4), pady=6, sticky="ew")
|
|
517
|
+
|
|
518
|
+
expand_btn = ctk.CTkButton(
|
|
519
|
+
row, text="›", width=28, height=28,
|
|
520
|
+
fg_color=("#e0e0e0", "#3a3a3a"),
|
|
521
|
+
hover_color=("#c8c8c8", "#484848"),
|
|
522
|
+
text_color=("#333333", "#eeeeee"),
|
|
523
|
+
corner_radius=8, border_width=0,
|
|
524
|
+
font=ctk.CTkFont(size=16, weight="bold"))
|
|
525
|
+
expand_btn.grid(row=0, column=4, padx=(0, 8), pady=6)
|
|
526
|
+
|
|
527
|
+
children_frame = ctk.CTkFrame(wrapper, fg_color=("#ececec", "#242424"), corner_radius=8)
|
|
528
|
+
child_rows = []
|
|
529
|
+
expanded = [False]
|
|
530
|
+
|
|
531
|
+
def _sync_children(*_, pv=var, cr=child_rows):
|
|
532
|
+
state = pv.get()
|
|
533
|
+
for _, cv in cr:
|
|
534
|
+
cv.set(state)
|
|
535
|
+
|
|
536
|
+
var.trace_add("write", _sync_children)
|
|
537
|
+
|
|
538
|
+
def _populate_children(p=path, cf=children_frame, cr=child_rows, pv=var):
|
|
539
|
+
if cr:
|
|
540
|
+
return
|
|
541
|
+
try:
|
|
542
|
+
entries = sorted(os.scandir(p), key=lambda e: (not e.is_dir(), e.name.lower()))
|
|
543
|
+
except PermissionError:
|
|
544
|
+
ctk.CTkLabel(cf, text=" ⚠️ Permission denied",
|
|
545
|
+
font=ctk.CTkFont(size=10), text_color="#FF9500"
|
|
546
|
+
).pack(anchor="w", padx=12, pady=4)
|
|
547
|
+
return
|
|
548
|
+
for entry in entries:
|
|
549
|
+
child_var = ctk.BooleanVar(value=pv.get())
|
|
550
|
+
child_row = ctk.CTkFrame(cf, fg_color="transparent")
|
|
551
|
+
child_row.pack(fill="x", padx=8, pady=1)
|
|
552
|
+
child_row.grid_columnconfigure(1, weight=1)
|
|
553
|
+
ctk.CTkCheckBox(child_row, text="", variable=child_var, width=20
|
|
554
|
+
).grid(row=0, column=0, padx=(8, 4), pady=3)
|
|
555
|
+
icon = "📁" if entry.is_dir() else "📄"
|
|
556
|
+
ctk.CTkLabel(child_row, text=f"{icon} {entry.name}",
|
|
557
|
+
font=ctk.CTkFont(size=10), anchor="w"
|
|
558
|
+
).grid(row=0, column=1, padx=2, pady=3, sticky="ew")
|
|
559
|
+
try:
|
|
560
|
+
sz_str = self.scanner.format_size(entry.stat().st_size) if entry.is_file() else ""
|
|
561
|
+
except Exception:
|
|
562
|
+
sz_str = ""
|
|
563
|
+
if sz_str:
|
|
564
|
+
ctk.CTkLabel(child_row, text=sz_str, text_color="#FF9500",
|
|
565
|
+
font=ctk.CTkFont(size=10, weight="bold")
|
|
566
|
+
).grid(row=0, column=2, padx=(0, 10), pady=3)
|
|
567
|
+
cr.append((entry.path, child_var))
|
|
568
|
+
|
|
569
|
+
def _toggle_expand(cf=children_frame, btn=expand_btn, ex=expanded,
|
|
570
|
+
populate=_populate_children):
|
|
571
|
+
populate()
|
|
572
|
+
if ex[0]:
|
|
573
|
+
cf.pack_forget()
|
|
574
|
+
btn.configure(text="›")
|
|
575
|
+
else:
|
|
576
|
+
cf.pack(fill="x", pady=(2, 0))
|
|
577
|
+
btn.configure(text="⌄")
|
|
578
|
+
ex[0] = not ex[0]
|
|
579
|
+
|
|
580
|
+
expand_btn.configure(command=_toggle_expand)
|
|
581
|
+
|
|
582
|
+
def _toggle(e, v=var):
|
|
583
|
+
v.set(not v.get())
|
|
584
|
+
|
|
585
|
+
for w in (size_lbl, path_lbl, row):
|
|
586
|
+
w.bind("<Button-1>", _toggle)
|
|
587
|
+
|
|
588
|
+
self.residue_rows.append((wrapper, cat, var, path, children_frame, child_rows))
|
|
589
|
+
|
|
590
|
+
# ── Filters ───────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
def set_all_visible(self, state: bool):
|
|
593
|
+
for entry in self.residue_rows:
|
|
594
|
+
wrapper, cat, var = entry[0], entry[1], entry[2]
|
|
595
|
+
if self.active_filter in ("All", cat):
|
|
596
|
+
var.set(state)
|
|
597
|
+
|
|
598
|
+
def create_filter_bubbles(self, categories):
|
|
599
|
+
for btn in self.filter_buttons.values():
|
|
600
|
+
btn.destroy()
|
|
601
|
+
self.filter_buttons = {}
|
|
602
|
+
|
|
603
|
+
for cat in categories:
|
|
604
|
+
btn = ctk.CTkButton(
|
|
605
|
+
self.bubbles_container, text=cat,
|
|
606
|
+
width=max(70, len(cat) * 8), height=28,
|
|
607
|
+
corner_radius=14, border_width=1, border_color="#757575",
|
|
608
|
+
fg_color="transparent", hover_color=("#c0c0c0", "#3d3d3d"),
|
|
609
|
+
text_color=("#333333", "#eeeeee"),
|
|
610
|
+
command=lambda c=cat: self.apply_filter(c))
|
|
611
|
+
btn.pack(side="left", padx=5)
|
|
612
|
+
self.filter_buttons[cat] = btn
|
|
613
|
+
|
|
614
|
+
if self.filter_buttons:
|
|
615
|
+
first = list(self.filter_buttons.keys())[0]
|
|
616
|
+
self.update_bubble_selection(first)
|
|
617
|
+
|
|
618
|
+
def update_bubble_selection(self, selected):
|
|
619
|
+
self.active_filter = selected
|
|
620
|
+
for cat, btn in self.filter_buttons.items():
|
|
621
|
+
if cat == selected:
|
|
622
|
+
btn.configure(fg_color="#1f538d", border_width=0, text_color="white")
|
|
623
|
+
else:
|
|
624
|
+
btn.configure(fg_color="transparent", border_width=1,
|
|
625
|
+
text_color=("#333333", "#eeeeee"))
|
|
626
|
+
|
|
627
|
+
def apply_filter(self, selection):
|
|
628
|
+
if selection in ("Scanning...", "No results", "Everything clean! ✨"):
|
|
629
|
+
return
|
|
630
|
+
self.update_bubble_selection(selection)
|
|
631
|
+
for entry in self.residue_rows:
|
|
632
|
+
wrapper, cat = entry[0], entry[1]
|
|
633
|
+
if selection == "All" or selection == cat:
|
|
634
|
+
wrapper.pack(fill="x", padx=4, pady=2)
|
|
635
|
+
else:
|
|
636
|
+
wrapper.pack_forget()
|
|
637
|
+
|
|
638
|
+
# ── Cleaning ──────────────────────────────────────────────────
|
|
639
|
+
|
|
640
|
+
def clean_selected(self):
|
|
641
|
+
count = 0
|
|
642
|
+
to_remove = []
|
|
643
|
+
for entry in list(self.residue_rows):
|
|
644
|
+
wrapper, cat, var, path = entry[0], entry[1], entry[2], entry[3]
|
|
645
|
+
child_rows = entry[5] if len(entry) > 5 else []
|
|
646
|
+
selected_children = [(cp, cv) for cp, cv in child_rows if cv.get()]
|
|
647
|
+
all_selected = child_rows and all(cv.get() for _, cv in child_rows)
|
|
648
|
+
|
|
649
|
+
if var.get() and (not child_rows or all_selected):
|
|
650
|
+
ok, msg = self.scanner.delete_folder(path)
|
|
651
|
+
self.log(msg)
|
|
652
|
+
if ok:
|
|
653
|
+
wrapper.destroy()
|
|
654
|
+
if len(entry) > 4:
|
|
655
|
+
entry[4].destroy()
|
|
656
|
+
to_remove.append(entry)
|
|
657
|
+
count += 1
|
|
658
|
+
elif selected_children:
|
|
659
|
+
for cp, cv in selected_children:
|
|
660
|
+
ok, msg = self.scanner.delete_folder(cp)
|
|
661
|
+
self.log(msg)
|
|
662
|
+
if ok:
|
|
663
|
+
count += 1
|
|
664
|
+
|
|
665
|
+
for r in to_remove:
|
|
666
|
+
self.residue_rows = [e for e in self.residue_rows if e is not r]
|
|
667
|
+
|
|
668
|
+
if count == 0:
|
|
669
|
+
self.log("No items selected.")
|
|
670
|
+
else:
|
|
671
|
+
self.status_label.configure(text=f"Cleaned {count} items.")
|
|
672
|
+
self._post_clean()
|
|
673
|
+
|
|
674
|
+
def clean_all(self):
|
|
675
|
+
count = 0
|
|
676
|
+
to_remove = []
|
|
677
|
+
for entry in list(self.residue_rows):
|
|
678
|
+
wrapper, cat, var, path = entry[0], entry[1], entry[2], entry[3]
|
|
679
|
+
if self.active_filter not in ("All", cat):
|
|
680
|
+
continue
|
|
681
|
+
ok, msg = self.scanner.delete_folder(path)
|
|
682
|
+
self.log(msg)
|
|
683
|
+
if ok:
|
|
684
|
+
wrapper.destroy()
|
|
685
|
+
if len(entry) > 4:
|
|
686
|
+
entry[4].destroy()
|
|
687
|
+
to_remove.append(entry)
|
|
688
|
+
count += 1
|
|
689
|
+
|
|
690
|
+
for r in to_remove:
|
|
691
|
+
self.residue_rows = [e for e in self.residue_rows if e is not r]
|
|
692
|
+
|
|
693
|
+
self.status_label.configure(text=f"Removed {count} items.")
|
|
694
|
+
self._post_clean()
|
|
695
|
+
|
|
696
|
+
def _post_clean(self):
|
|
697
|
+
if not self.residue_rows:
|
|
698
|
+
self.status_label.configure(text="Everything clean! ✨")
|
|
699
|
+
self.clean_selected_button.configure(state="disabled")
|
|
700
|
+
self.clean_all_button.configure(state="disabled")
|
|
701
|
+
self.create_filter_bubbles(["Everything clean! ✨"])
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def main():
|
|
705
|
+
app = BrainCleanerApp()
|
|
706
|
+
app.mainloop()
|
|
707
|
+
|
|
708
|
+
if __name__ == "__main__":
|
|
709
|
+
main()
|