1use super::{AppCategory, AppInfo, OptimizationAction, OptimizationResult};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use sysinfo::{System, ProcessesToUpdate, Pid};
21
22#[derive(Debug, Clone)]
24pub struct ElectronAppPattern {
25 pub name: &'static str,
26 pub display_name: &'static str,
27 pub patterns: &'static [&'static str],
28 pub category: AppCategory,
29 pub baseline_memory_mb: f64,
31}
32
33pub const ELECTRON_APPS: &[ElectronAppPattern] = &[
35 ElectronAppPattern {
36 name: "vscode",
37 display_name: "Visual Studio Code",
38 patterns: &["code", "code.exe", "code helper", "code - insiders"],
39 category: AppCategory::Development,
40 baseline_memory_mb: 300.0,
41 },
42 ElectronAppPattern {
43 name: "discord",
44 display_name: "Discord",
45 patterns: &["discord", "discord.exe", "discord helper"],
46 category: AppCategory::Communication,
47 baseline_memory_mb: 200.0,
48 },
49 ElectronAppPattern {
50 name: "slack",
51 display_name: "Slack",
52 patterns: &["slack", "slack.exe", "slack helper"],
53 category: AppCategory::Communication,
54 baseline_memory_mb: 250.0,
55 },
56 ElectronAppPattern {
57 name: "teams",
58 display_name: "Microsoft Teams",
59 patterns: &["teams", "ms-teams", "microsoft teams", "teams.exe"],
60 category: AppCategory::Communication,
61 baseline_memory_mb: 300.0,
62 },
63 ElectronAppPattern {
64 name: "notion",
65 display_name: "Notion",
66 patterns: &["notion", "notion.exe", "notion helper"],
67 category: AppCategory::Development,
68 baseline_memory_mb: 200.0,
69 },
70 ElectronAppPattern {
71 name: "figma",
72 display_name: "Figma",
73 patterns: &["figma", "figma.exe", "figma helper", "figma agent"],
74 category: AppCategory::Creative,
75 baseline_memory_mb: 400.0,
76 },
77 ElectronAppPattern {
78 name: "spotify",
79 display_name: "Spotify",
80 patterns: &["spotify", "spotify.exe", "spotify helper"],
81 category: AppCategory::Media,
82 baseline_memory_mb: 150.0,
83 },
84 ElectronAppPattern {
85 name: "obsidian",
86 display_name: "Obsidian",
87 patterns: &["obsidian", "obsidian.exe", "obsidian helper"],
88 category: AppCategory::Development,
89 baseline_memory_mb: 150.0,
90 },
91 ElectronAppPattern {
92 name: "1password",
93 display_name: "1Password",
94 patterns: &["1password", "1password.exe", "1password helper"],
95 category: AppCategory::System,
96 baseline_memory_mb: 100.0,
97 },
98 ElectronAppPattern {
99 name: "postman",
100 display_name: "Postman",
101 patterns: &["postman", "postman.exe", "postman helper"],
102 category: AppCategory::Development,
103 baseline_memory_mb: 300.0,
104 },
105 ElectronAppPattern {
106 name: "whatsapp",
107 display_name: "WhatsApp",
108 patterns: &["whatsapp", "whatsapp.exe", "whatsapp helper"],
109 category: AppCategory::Communication,
110 baseline_memory_mb: 150.0,
111 },
112 ElectronAppPattern {
113 name: "signal",
114 display_name: "Signal",
115 patterns: &["signal", "signal.exe", "signal helper"],
116 category: AppCategory::Communication,
117 baseline_memory_mb: 150.0,
118 },
119 ElectronAppPattern {
120 name: "telegram",
121 display_name: "Telegram Desktop",
122 patterns: &["telegram", "telegram.exe", "telegram desktop"],
123 category: AppCategory::Communication,
124 baseline_memory_mb: 150.0,
125 },
126 ElectronAppPattern {
127 name: "cursor",
128 display_name: "Cursor",
129 patterns: &["cursor", "cursor.exe", "cursor helper"],
130 category: AppCategory::Development,
131 baseline_memory_mb: 300.0,
132 },
133 ElectronAppPattern {
134 name: "windsurf",
135 display_name: "Windsurf",
136 patterns: &["windsurf", "windsurf.exe", "windsurf helper"],
137 category: AppCategory::Development,
138 baseline_memory_mb: 300.0,
139 },
140 ElectronAppPattern {
141 name: "zed",
142 display_name: "Zed",
143 patterns: &["zed", "zed.exe", "zed helper"],
144 category: AppCategory::Development,
145 baseline_memory_mb: 200.0,
146 },
147 ElectronAppPattern {
148 name: "linear",
149 display_name: "Linear",
150 patterns: &["linear", "linear.exe", "linear helper"],
151 category: AppCategory::Development,
152 baseline_memory_mb: 200.0,
153 },
154 ElectronAppPattern {
155 name: "hyper",
156 display_name: "Hyper Terminal",
157 patterns: &["hyper", "hyper.exe", "hyper helper"],
158 category: AppCategory::Development,
159 baseline_memory_mb: 150.0,
160 },
161 ElectronAppPattern {
162 name: "atom",
163 display_name: "Atom",
164 patterns: &["atom", "atom.exe", "atom helper"],
165 category: AppCategory::Development,
166 baseline_memory_mb: 250.0,
167 },
168 ElectronAppPattern {
169 name: "bitwarden",
170 display_name: "Bitwarden",
171 patterns: &["bitwarden", "bitwarden.exe", "bitwarden helper"],
172 category: AppCategory::System,
173 baseline_memory_mb: 100.0,
174 },
175 ElectronAppPattern {
176 name: "mongodb-compass",
177 display_name: "MongoDB Compass",
178 patterns: &["mongodb compass", "mongodb-compass", "compass"],
179 category: AppCategory::Development,
180 baseline_memory_mb: 300.0,
181 },
182 ElectronAppPattern {
183 name: "insomnia",
184 display_name: "Insomnia",
185 patterns: &["insomnia", "insomnia.exe"],
186 category: AppCategory::Development,
187 baseline_memory_mb: 200.0,
188 },
189 ElectronAppPattern {
190 name: "loom",
191 display_name: "Loom",
192 patterns: &["loom", "loom.exe", "loom helper"],
193 category: AppCategory::Media,
194 baseline_memory_mb: 200.0,
195 },
196 ElectronAppPattern {
197 name: "gitkraken",
198 display_name: "GitKraken",
199 patterns: &["gitkraken", "gitkraken.exe"],
200 category: AppCategory::Development,
201 baseline_memory_mb: 250.0,
202 },
203];
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ElectronAppInfo {
208 pub name: String,
209 pub display_name: String,
210 pub category: AppCategory,
211 pub total_memory_mb: f64,
212 pub total_cpu_percent: f32,
213 pub process_count: usize,
214 pub main_pid: Option<u32>,
215 pub pids: Vec<u32>,
216 pub baseline_memory_mb: f64,
217 pub memory_overhead_percent: f64,
218 pub is_running: bool,
219}
220
221impl ElectronAppInfo {
222 pub fn is_bloated(&self) -> bool {
224 self.total_memory_mb > self.baseline_memory_mb * 2.0
225 }
226
227 pub fn likely_memory_leak(&self) -> bool {
229 self.memory_overhead_percent > 200.0
230 }
231
232 pub fn get_suggested_action(&self) -> OptimizationAction {
234 if self.total_memory_mb > 1500.0 {
235 OptimizationAction::Restart
236 } else if self.is_bloated() {
237 OptimizationAction::TrimMemory
238 } else {
239 OptimizationAction::None
240 }
241 }
242}
243
244pub struct ElectronManager {
246 system: System,
247 apps: HashMap<String, ElectronAppInfo>,
248 last_update: std::time::Instant,
249}
250
251impl ElectronManager {
252 pub fn new() -> Self {
253 let mut system = System::new_all();
254 system.refresh_processes(ProcessesToUpdate::All, true);
255
256 Self {
257 system,
258 apps: HashMap::new(),
259 last_update: std::time::Instant::now(),
260 }
261 }
262
263 pub fn refresh(&mut self) {
265 self.system.refresh_processes(ProcessesToUpdate::All, true);
266 self.detect_electron_apps();
267 self.last_update = std::time::Instant::now();
268 }
269
270 fn detect_electron_apps(&mut self) {
272 self.apps.clear();
273
274 for pattern in ELECTRON_APPS {
275 let mut app_info = ElectronAppInfo {
276 name: pattern.name.to_string(),
277 display_name: pattern.display_name.to_string(),
278 category: pattern.category,
279 total_memory_mb: 0.0,
280 total_cpu_percent: 0.0,
281 process_count: 0,
282 main_pid: None,
283 pids: Vec::new(),
284 baseline_memory_mb: pattern.baseline_memory_mb,
285 memory_overhead_percent: 0.0,
286 is_running: false,
287 };
288
289 for (pid, process) in self.system.processes() {
290 let name = process.name().to_string_lossy().to_lowercase();
291
292 let matches = pattern.patterns.iter().any(|p| name.contains(p));
294
295 if matches {
296 let memory_mb = process.memory() as f64 / (1024.0 * 1024.0);
297 let cpu_percent = process.cpu_usage();
298
299 app_info.total_memory_mb += memory_mb;
300 app_info.total_cpu_percent += cpu_percent;
301 app_info.process_count += 1;
302 app_info.pids.push(pid.as_u32());
303 app_info.is_running = true;
304
305 if app_info.main_pid.is_none() && !name.contains("helper") {
307 app_info.main_pid = Some(pid.as_u32());
308 }
309 }
310 }
311
312 if app_info.is_running {
313 app_info.memory_overhead_percent =
315 (app_info.total_memory_mb / app_info.baseline_memory_mb) * 100.0;
316
317 self.apps.insert(pattern.name.to_string(), app_info);
318 }
319 }
320 }
321
322 pub fn get_apps(&self) -> Vec<&ElectronAppInfo> {
324 self.apps.values().collect()
325 }
326
327 pub fn get_app(&self, name: &str) -> Option<&ElectronAppInfo> {
329 self.apps.get(name)
330 }
331
332 pub fn total_memory_mb(&self) -> f64 {
334 self.apps.values().map(|a| a.total_memory_mb).sum()
335 }
336
337 pub fn get_bloated_apps(&self) -> Vec<&ElectronAppInfo> {
339 self.apps.values().filter(|a| a.is_bloated()).collect()
340 }
341
342 pub fn get_suggestions(&self) -> Vec<(String, OptimizationAction, String)> {
344 let mut suggestions = Vec::new();
345
346 for app in self.apps.values() {
347 let action = app.get_suggested_action();
348 if action != OptimizationAction::None {
349 let reason = match &action {
350 OptimizationAction::Restart => {
351 format!(
352 "{} is using {:.0} MB ({:.0}% of baseline) - restart recommended",
353 app.display_name,
354 app.total_memory_mb,
355 app.memory_overhead_percent
356 )
357 }
358 OptimizationAction::TrimMemory => {
359 format!(
360 "{} is using {:.0} MB ({:.0}% of expected {:.0} MB)",
361 app.display_name,
362 app.total_memory_mb,
363 app.memory_overhead_percent,
364 app.baseline_memory_mb
365 )
366 }
367 _ => continue,
368 };
369
370 suggestions.push((app.display_name.clone(), action, reason));
371 }
372 }
373
374 suggestions.sort_by(|a, b| {
375 let mem_a = self.apps.values().find(|app| app.display_name == a.0)
376 .map(|a| a.total_memory_mb).unwrap_or(0.0);
377 let mem_b = self.apps.values().find(|app| app.display_name == b.0)
378 .map(|a| a.total_memory_mb).unwrap_or(0.0);
379 mem_b.partial_cmp(&mem_a).unwrap()
380 });
381
382 suggestions
383 }
384
385 pub fn print_summary(&self) {
387 println!("\n⚡ Electron Apps Memory Usage\n");
388 println!("┌──────────────────────┬───────────┬──────────┬───────────┬──────────┐");
389 println!("│ Application │ Memory │ Baseline │ Overhead │ Status │");
390 println!("├──────────────────────┼───────────┼──────────┼───────────┼──────────┤");
391
392 let mut apps: Vec<_> = self.apps.values().collect();
393 apps.sort_by(|a, b| b.total_memory_mb.partial_cmp(&a.total_memory_mb).unwrap());
394
395 for app in &apps {
396 let status = if app.total_memory_mb > 1000.0 {
397 "🔴 Heavy"
398 } else if app.is_bloated() {
399 "🟡 Bloated"
400 } else {
401 "🟢 Normal"
402 };
403
404 println!(
405 "│ {:20} │ {:>7.0} MB │ {:>6.0} MB │ {:>8.0}% │ {:8} │",
406 truncate(&app.display_name, 20),
407 app.total_memory_mb,
408 app.baseline_memory_mb,
409 app.memory_overhead_percent,
410 status
411 );
412 }
413
414 println!("└──────────────────────┴───────────┴──────────┴───────────┴──────────┘");
415
416 let total: f64 = apps.iter().map(|a| a.total_memory_mb).sum();
417 println!(
418 "\nTotal: {:.0} MB across {} Electron apps ({} processes)",
419 total,
420 apps.len(),
421 apps.iter().map(|a| a.process_count).sum::<usize>()
422 );
423
424 let bloated = self.get_bloated_apps();
425 if !bloated.is_empty() {
426 println!("\n💡 Suggestions:");
427 for app in bloated.iter().take(3) {
428 println!(
429 " • {} is using {:.0}% more memory than expected - consider restarting",
430 app.display_name,
431 app.memory_overhead_percent - 100.0
432 );
433 }
434 }
435 }
436}
437
438impl Default for ElectronManager {
439 fn default() -> Self {
440 Self::new()
441 }
442}
443
444fn truncate(s: &str, max: usize) -> String {
445 if s.len() <= max {
446 format!("{:width$}", s, width = max)
447 } else {
448 format!("{}...", &s[..max - 3])
449 }
450}