ruvector_memopt/tray/
mod.rs

1//! System tray icon and menu with automatic optimization and AI Mode settings
2
3use crate::windows::memory::WindowsMemoryOptimizer;
4use crate::accel::CpuCapabilities;
5use std::sync::{Arc, atomic::{AtomicBool, AtomicU32, Ordering}};
6use tray_icon::{
7    menu::{Menu, MenuEvent, MenuItem, CheckMenuItem, Submenu, PredefinedMenuItem},
8    TrayIconBuilder, Icon,
9};
10use winit::event_loop::{ControlFlow, EventLoop};
11
12/// Auto-optimization threshold (optimize when memory usage exceeds this %)
13pub const AUTO_OPTIMIZE_THRESHOLD: u32 = 75;
14/// Auto-optimization interval in seconds
15pub const AUTO_OPTIMIZE_INTERVAL: u64 = 60;
16
17/// GitHub repository URL
18const GITHUB_URL: &str = "https://github.com/ruvnet/optimizer";
19/// Version string
20const VERSION: &str = env!("CARGO_PKG_VERSION");
21
22pub struct TrayApp {
23    running: Arc<AtomicBool>,
24}
25
26impl TrayApp {
27    pub fn new() -> Self {
28        Self {
29            running: Arc::new(AtomicBool::new(true)),
30        }
31    }
32
33    pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
34        let event_loop = EventLoop::new()?;
35
36        // Get initial memory status
37        let status_text = get_memory_status_text();
38
39        // Create menu items
40        let menu = Menu::new();
41
42        // Status section
43        let status_item = MenuItem::new(&status_text, false, None);
44        let version_item = MenuItem::new(&format!("v{}", VERSION), false, None);
45
46        // Main actions
47        let auto_item = CheckMenuItem::new("Auto-Optimize (60s)", true, true, None);
48        let optimize_item = MenuItem::new("Optimize Now", true, None);
49        let aggressive_item = MenuItem::new("Deep Clean", true, None);
50
51        // AI Mode submenu
52        let ai_menu = Submenu::new("AI Mode", true);
53        let game_mode_item = CheckMenuItem::new("Game Mode Auto-Detect", true, true, None);
54        let focus_mode_item = CheckMenuItem::new("Focus Mode Auto-Detect", true, true, None);
55        let thermal_item = CheckMenuItem::new("Thermal Prediction", true, true, None);
56        let preload_item = CheckMenuItem::new("Predictive Preloading", true, true, None);
57        ai_menu.append(&game_mode_item)?;
58        ai_menu.append(&focus_mode_item)?;
59        ai_menu.append(&PredefinedMenuItem::separator())?;
60        ai_menu.append(&thermal_item)?;
61        ai_menu.append(&preload_item)?;
62
63        // Settings submenu
64        let settings_menu = Submenu::new("Settings", true);
65        let threshold_75 = CheckMenuItem::new("Threshold: 75%", true, true, None);
66        let threshold_80 = CheckMenuItem::new("Threshold: 80%", true, false, None);
67        let threshold_85 = CheckMenuItem::new("Threshold: 85%", true, false, None);
68        let threshold_90 = CheckMenuItem::new("Threshold: 90%", true, false, None);
69        settings_menu.append(&threshold_75)?;
70        settings_menu.append(&threshold_80)?;
71        settings_menu.append(&threshold_85)?;
72        settings_menu.append(&threshold_90)?;
73
74        // Info section
75        let cpu_item = MenuItem::new("System Info", true, None);
76        let github_item = MenuItem::new("GitHub Repository", true, None);
77        let quit_item = MenuItem::new("Quit", true, None);
78
79        // Build menu
80        menu.append(&status_item)?;
81        menu.append(&version_item)?;
82        menu.append(&PredefinedMenuItem::separator())?;
83        menu.append(&auto_item)?;
84        menu.append(&optimize_item)?;
85        menu.append(&aggressive_item)?;
86        menu.append(&PredefinedMenuItem::separator())?;
87        menu.append(&ai_menu)?;
88        menu.append(&settings_menu)?;
89        menu.append(&PredefinedMenuItem::separator())?;
90        menu.append(&cpu_item)?;
91        menu.append(&github_item)?;
92        menu.append(&PredefinedMenuItem::separator())?;
93        menu.append(&quit_item)?;
94
95        // Get initial memory usage for icon
96        let initial_usage = WindowsMemoryOptimizer::get_memory_status()
97            .map(|s| s.memory_load_percent)
98            .unwrap_or(50);
99
100        // Create tray icon with current usage
101        let icon_data = create_icon_with_usage(initial_usage);
102        let icon = Icon::from_rgba(icon_data, 32, 32)?;
103
104        let tray_icon = TrayIconBuilder::new()
105            .with_menu(Box::new(menu))
106            .with_tooltip(&format!("RuVector MemOpt v{} - Auto-optimizing", VERSION))
107            .with_icon(icon)
108            .build()?;
109
110        // Store menu item IDs
111        let optimize_id = optimize_item.id().clone();
112        let aggressive_id = aggressive_item.id().clone();
113        let cpu_id = cpu_item.id().clone();
114        let quit_id = quit_item.id().clone();
115        let auto_id = auto_item.id().clone();
116        let github_id = github_item.id().clone();
117        let game_mode_id = game_mode_item.id().clone();
118        let focus_mode_id = focus_mode_item.id().clone();
119        let thermal_id = thermal_item.id().clone();
120        let preload_id = preload_item.id().clone();
121        let threshold_75_id = threshold_75.id().clone();
122        let threshold_80_id = threshold_80.id().clone();
123        let threshold_85_id = threshold_85.id().clone();
124        let threshold_90_id = threshold_90.id().clone();
125
126        let running = self.running.clone();
127        let mut last_update = std::time::Instant::now();
128        let mut last_auto_optimize = std::time::Instant::now();
129        let auto_enabled = Arc::new(AtomicBool::new(true));
130        let game_mode_enabled = Arc::new(AtomicBool::new(true));
131        let focus_mode_enabled = Arc::new(AtomicBool::new(true));
132        let thermal_enabled = Arc::new(AtomicBool::new(true));
133        let preload_enabled = Arc::new(AtomicBool::new(true));
134        let current_threshold = Arc::new(AtomicU32::new(75));
135        let last_usage = Arc::new(AtomicU32::new(initial_usage));
136        let total_freed = Arc::new(AtomicU32::new(0));
137
138        // Run event loop
139        #[allow(deprecated)]
140        event_loop.run(move |_event, event_loop| {
141            event_loop.set_control_flow(ControlFlow::WaitUntil(
142                std::time::Instant::now() + std::time::Duration::from_secs(1)
143            ));
144
145            // Update status and check for auto-optimization every 5 seconds
146            if last_update.elapsed() > std::time::Duration::from_secs(5) {
147                if let Ok(status) = WindowsMemoryOptimizer::get_memory_status() {
148                    let usage = status.memory_load_percent;
149                    last_usage.store(usage, Ordering::SeqCst);
150
151                    // Update status text
152                    let freed = total_freed.load(Ordering::SeqCst);
153                    let threshold = current_threshold.load(Ordering::SeqCst);
154                    let text = if freed > 0 {
155                        format!(
156                            "Memory: {:.0}% ({:.1}/{:.1} GB) | Freed: {} MB",
157                            usage,
158                            status.used_physical_mb() / 1024.0,
159                            status.total_physical_mb / 1024.0,
160                            freed
161                        )
162                    } else {
163                        format!(
164                            "Memory: {:.0}% ({:.1}/{:.1} GB)",
165                            usage,
166                            status.used_physical_mb() / 1024.0,
167                            status.total_physical_mb / 1024.0
168                        )
169                    };
170                    let _ = status_item.set_text(&text);
171
172                    // Update icon color based on usage
173                    let icon_data = create_icon_with_usage(usage);
174                    if let Ok(new_icon) = Icon::from_rgba(icon_data, 32, 32) {
175                        let _ = tray_icon.set_icon(Some(new_icon));
176                    }
177
178                    // Update tooltip with mode info
179                    let modes = build_mode_string(
180                        game_mode_enabled.load(Ordering::SeqCst),
181                        focus_mode_enabled.load(Ordering::SeqCst),
182                    );
183                    let tooltip = if auto_enabled.load(Ordering::SeqCst) {
184                        format!("RuVector v{} - {}% | Auto @{}%{}", VERSION, usage, threshold, modes)
185                    } else {
186                        format!("RuVector v{} - {}% | Manual{}", VERSION, usage, modes)
187                    };
188                    let _ = tray_icon.set_tooltip(Some(tooltip));
189
190                    // Auto-optimize if enabled and conditions met
191                    if auto_enabled.load(Ordering::SeqCst)
192                        && usage > threshold
193                        && last_auto_optimize.elapsed() > std::time::Duration::from_secs(AUTO_OPTIMIZE_INTERVAL)
194                    {
195                        let total_freed_clone = total_freed.clone();
196                        std::thread::spawn(move || {
197                            let optimizer = WindowsMemoryOptimizer::new();
198                            if let Ok(result) = optimizer.optimize(false) {
199                                if result.freed_mb > 100.0 {
200                                    let current = total_freed_clone.load(Ordering::SeqCst);
201                                    total_freed_clone.store(current + result.freed_mb as u32, Ordering::SeqCst);
202                                }
203                            }
204                        });
205                        last_auto_optimize = std::time::Instant::now();
206                    }
207                }
208                last_update = std::time::Instant::now();
209            }
210
211            // Handle menu events
212            if let Ok(event) = MenuEvent::receiver().try_recv() {
213                if event.id == quit_id {
214                    running.store(false, Ordering::SeqCst);
215                    event_loop.exit();
216                } else if event.id == optimize_id {
217                    let total_freed_clone = total_freed.clone();
218                    run_optimization(false, total_freed_clone);
219                } else if event.id == aggressive_id {
220                    let total_freed_clone = total_freed.clone();
221                    run_optimization(true, total_freed_clone);
222                } else if event.id == cpu_id {
223                    show_cpu_info();
224                } else if event.id == github_id {
225                    open_github();
226                } else if event.id == auto_id {
227                    let current = auto_enabled.load(Ordering::SeqCst);
228                    auto_enabled.store(!current, Ordering::SeqCst);
229                    let _ = auto_item.set_checked(!current);
230                } else if event.id == game_mode_id {
231                    let current = game_mode_enabled.load(Ordering::SeqCst);
232                    game_mode_enabled.store(!current, Ordering::SeqCst);
233                    let _ = game_mode_item.set_checked(!current);
234                } else if event.id == focus_mode_id {
235                    let current = focus_mode_enabled.load(Ordering::SeqCst);
236                    focus_mode_enabled.store(!current, Ordering::SeqCst);
237                    let _ = focus_mode_item.set_checked(!current);
238                } else if event.id == thermal_id {
239                    let current = thermal_enabled.load(Ordering::SeqCst);
240                    thermal_enabled.store(!current, Ordering::SeqCst);
241                    let _ = thermal_item.set_checked(!current);
242                } else if event.id == preload_id {
243                    let current = preload_enabled.load(Ordering::SeqCst);
244                    preload_enabled.store(!current, Ordering::SeqCst);
245                    let _ = preload_item.set_checked(!current);
246                } else if event.id == threshold_75_id {
247                    current_threshold.store(75, Ordering::SeqCst);
248                    let _ = threshold_75.set_checked(true);
249                    let _ = threshold_80.set_checked(false);
250                    let _ = threshold_85.set_checked(false);
251                    let _ = threshold_90.set_checked(false);
252                } else if event.id == threshold_80_id {
253                    current_threshold.store(80, Ordering::SeqCst);
254                    let _ = threshold_75.set_checked(false);
255                    let _ = threshold_80.set_checked(true);
256                    let _ = threshold_85.set_checked(false);
257                    let _ = threshold_90.set_checked(false);
258                } else if event.id == threshold_85_id {
259                    current_threshold.store(85, Ordering::SeqCst);
260                    let _ = threshold_75.set_checked(false);
261                    let _ = threshold_80.set_checked(false);
262                    let _ = threshold_85.set_checked(true);
263                    let _ = threshold_90.set_checked(false);
264                } else if event.id == threshold_90_id {
265                    current_threshold.store(90, Ordering::SeqCst);
266                    let _ = threshold_75.set_checked(false);
267                    let _ = threshold_80.set_checked(false);
268                    let _ = threshold_85.set_checked(false);
269                    let _ = threshold_90.set_checked(true);
270                }
271            }
272        })?;
273
274        Ok(())
275    }
276}
277
278fn build_mode_string(game: bool, focus: bool) -> String {
279    let mut modes = Vec::new();
280    if game { modes.push("Game"); }
281    if focus { modes.push("Focus"); }
282    if modes.is_empty() {
283        String::new()
284    } else {
285        format!(" | {}", modes.join("+"))
286    }
287}
288
289fn get_memory_status_text() -> String {
290    if let Ok(status) = WindowsMemoryOptimizer::get_memory_status() {
291        format!(
292            "Memory: {:.0}% ({:.1}/{:.1} GB)",
293            status.memory_load_percent,
294            status.used_physical_mb() / 1024.0,
295            status.total_physical_mb / 1024.0
296        )
297    } else {
298        "Memory: Unknown".to_string()
299    }
300}
301
302fn run_optimization(aggressive: bool, total_freed: Arc<AtomicU32>) {
303    std::thread::spawn(move || {
304        let optimizer = WindowsMemoryOptimizer::new();
305        match optimizer.optimize(aggressive) {
306            Ok(result) => {
307                let current = total_freed.load(Ordering::SeqCst);
308                total_freed.store(current + result.freed_mb as u32, Ordering::SeqCst);
309
310                let msg = format!(
311                    "Optimization Complete!\n\nFreed: {:.1} MB\nProcesses: {}\nTime: {} ms",
312                    result.freed_mb, result.processes_trimmed, result.duration_ms
313                );
314                show_message_box("RuVector Optimizer", &msg);
315            }
316            Err(e) => {
317                show_message_box("RuVector Optimizer", &format!("Error: {}", e));
318            }
319        }
320    });
321}
322
323fn show_cpu_info() {
324    let caps = CpuCapabilities::detect();
325    let msg = format!(
326        "RuVector Memory Optimizer v{}\n\n\
327        CPU: {}\n\n\
328        Cores: {}\n\
329        AVX2: {}\n\
330        AVX-512: {}\n\
331        AVX-VNNI: {}\n\
332        Intel NPU: {}\n\n\
333        Estimated SIMD Speedup: {:.1}x\n\n\
334        GitHub: {}",
335        VERSION,
336        caps.model,
337        caps.core_count,
338        if caps.has_avx2 { "Yes" } else { "No" },
339        if caps.has_avx512 { "Yes" } else { "No" },
340        if caps.has_avx_vnni { "Yes" } else { "No" },
341        if caps.has_npu { "Yes" } else { "No" },
342        caps.estimated_speedup(),
343        GITHUB_URL
344    );
345    show_message_box("System Information", &msg);
346}
347
348fn open_github() {
349    #[cfg(windows)]
350    {
351        let _ = std::process::Command::new("cmd")
352            .args(["/C", "start", GITHUB_URL])
353            .spawn();
354    }
355}
356
357fn show_message_box(title: &str, message: &str) {
358    #[cfg(windows)]
359    {
360        use std::ffi::OsStr;
361        use std::os::windows::ffi::OsStrExt;
362        use std::ptr;
363
364        fn to_wide(s: &str) -> Vec<u16> {
365            OsStr::new(s).encode_wide().chain(Some(0)).collect()
366        }
367
368        let title = to_wide(title);
369        let message = to_wide(message);
370
371        unsafe {
372            windows::Win32::UI::WindowsAndMessaging::MessageBoxW(
373                windows::Win32::Foundation::HWND(ptr::null_mut()),
374                windows::core::PCWSTR(message.as_ptr()),
375                windows::core::PCWSTR(title.as_ptr()),
376                windows::Win32::UI::WindowsAndMessaging::MB_OK |
377                windows::Win32::UI::WindowsAndMessaging::MB_ICONINFORMATION,
378            );
379        }
380    }
381}
382
383/// Create icon with specific memory usage percentage for color coding
384fn create_icon_with_usage(usage_percent: u32) -> Vec<u8> {
385    let mut data = Vec::with_capacity(32 * 32 * 4);
386
387    // Color based on memory usage
388    let (r, g, b) = if usage_percent < 60 {
389        (0x00u8, 0xC8u8, 0x50u8) // Green
390    } else if usage_percent < 80 {
391        (0xFFu8, 0xA5u8, 0x00u8) // Orange
392    } else {
393        (0xE0u8, 0x30u8, 0x30u8) // Red
394    };
395
396    let (border_r, border_g, border_b) = if usage_percent < 60 {
397        (0x00u8, 0x80u8, 0x30u8)
398    } else if usage_percent < 80 {
399        (0xCCu8, 0x80u8, 0x00u8)
400    } else {
401        (0xA0u8, 0x20u8, 0x20u8)
402    };
403
404    for y in 0..32 {
405        for x in 0..32 {
406            // Memory chip shape: rounded rectangle with notch
407            let in_body = x >= 4 && x < 28 && y >= 2 && y < 30;
408            let in_notch = x >= 12 && x < 20 && y < 4;
409            let in_chip = in_body && !in_notch;
410
411            // Pin markers on sides
412            let left_pin = x < 4 && (y == 8 || y == 14 || y == 20 || y == 26);
413            let right_pin = x >= 28 && (y == 8 || y == 14 || y == 20 || y == 26);
414            let is_pin = left_pin || right_pin;
415
416            // Border detection
417            let is_border = in_chip && (x == 4 || x == 27 || y == 2 || y == 29 ||
418                                       (y == 3 && (x < 12 || x >= 20)));
419
420            // Fill level indicator (shows usage as filled portion)
421            let fill_height = 28 - ((usage_percent as i32 * 26) / 100);
422            let is_filled = in_chip && !is_border && (y as i32) >= fill_height;
423
424            if is_pin {
425                // Pins in border color
426                data.push(border_r);
427                data.push(border_g);
428                data.push(border_b);
429                data.push(0xFF);
430            } else if is_border {
431                // Border
432                data.push(border_r);
433                data.push(border_g);
434                data.push(border_b);
435                data.push(0xFF);
436            } else if is_filled {
437                // Filled portion (based on usage)
438                data.push(r);
439                data.push(g);
440                data.push(b);
441                data.push(0xFF);
442            } else if in_chip {
443                // Empty portion (darker)
444                data.push(r / 3);
445                data.push(g / 3);
446                data.push(b / 3);
447                data.push(0xFF);
448            } else {
449                // Transparent
450                data.push(0x00);
451                data.push(0x00);
452                data.push(0x00);
453                data.push(0x00);
454            }
455        }
456    }
457
458    data
459}
460
461impl Default for TrayApp {
462    fn default() -> Self {
463        Self::new()
464    }
465}