diff --git a/src/bin/test.rs b/src/bin/test.rs deleted file mode 100644 index 8600faa..0000000 --- a/src/bin/test.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - let payload = vimit::demo_payload(); - let windows = vimit::summarize_me(&payload, 75.0, 90.0); - for w in windows { - println!("{:?}", w); - } -} diff --git a/src/bin/vimit-gui.rs b/src/bin/vimit-gui.rs index 897a3b6..d1db9a6 100644 --- a/src/bin/vimit-gui.rs +++ b/src/bin/vimit-gui.rs @@ -17,7 +17,7 @@ use std::thread; use std::time::{Duration, Instant}; use tray_icon::{ Icon, MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent, - menu::{Menu, MenuEvent, MenuItem}, + menu::{Menu, MenuEvent, MenuId, MenuItem}, }; use vimit as ng; @@ -342,6 +342,52 @@ fn create_status_icon(color: (u8, u8, u8)) -> Icon { Icon::from_rgba(rgba, width as u32, height as u32).expect("failed to create tray icon") } +#[derive(Clone)] +struct TrayIds { + show: MenuId, + mini: MenuId, + quit: MenuId, +} + +fn try_init_tray() -> Option { + let tray_menu = Menu::new(); + let show_item = MenuItem::new("Показать окно", true, None); + let mini_item = MenuItem::new("Мини-окно", true, None); + let quit_item = MenuItem::new("Выход", true, None); + + for item in [&show_item, &mini_item, &quit_item] { + if let Err(error) = tray_menu.append(item) { + eprintln!("vimit-gui: tray menu unavailable: {error}"); + return None; + } + } + + let ids = TrayIds { + show: show_item.id().clone(), + mini: mini_item.id().clone(), + quit: quit_item.id().clone(), + }; + + let tray_icon_instance = match TrayIconBuilder::new() + .with_menu(Box::new(tray_menu)) + .with_tooltip("VibeMode Control") + .with_icon(create_status_icon((141, 150, 170))) + .build() + { + Ok(icon) => icon, + Err(error) => { + eprintln!("vimit-gui: tray unavailable: {error}"); + return None; + } + }; + + TRAY_ICON.with(|cell| { + *cell.borrow_mut() = Some(tray_icon_instance); + }); + + Some(ids) +} + fn spawn_mini_overlay(force_demo: bool) -> Result<(), String> { let current = std::env::current_exe().map_err(|error| format!("cannot find GUI exe: {error}"))?; @@ -560,37 +606,17 @@ fn main() { set_overlay_window_size(&app, is_compact); } - // Initialize System Tray - let tray_menu = Menu::new(); - let show_item = MenuItem::new("Показать окно", true, None); - let mini_item = MenuItem::new("Мини-окно", true, None); - let quit_item = MenuItem::new("Выход", true, None); - tray_menu.append(&show_item).unwrap(); - tray_menu.append(&mini_item).unwrap(); - tray_menu.append(&quit_item).unwrap(); - - let show_id = show_item.id().clone(); - let mini_id = mini_item.id().clone(); - let quit_id = quit_item.id().clone(); - - let tray_icon_instance = TrayIconBuilder::new() - .with_menu(Box::new(tray_menu)) - .with_tooltip("VibeMode Control") - .with_icon(create_status_icon((141, 150, 170))) // grey by default - .build() - .unwrap(); - - TRAY_ICON.with(|cell| { - *cell.borrow_mut() = Some(tray_icon_instance); - }); + let tray_ids = try_init_tray(); + let has_tray = tray_ids.is_some(); // Minimize to tray on close request let weak = app.as_weak(); app.window().on_close_requested(move || { - if let Some(app) = weak.upgrade() { + if has_tray && let Some(app) = weak.upgrade() { let _ = app.hide(); + return slint::CloseRequestResponse::KeepWindowShown; } - slint::CloseRequestResponse::KeepWindowShown + slint::CloseRequestResponse::HideWindow }); app.on_close_overlay(move || { @@ -684,20 +710,19 @@ fn main() { // Listen for Tray Events let tray_timer = Timer::default(); let weak = app.as_weak(); - let show_id_clone = show_id.clone(); - let mini_id_clone = mini_id.clone(); - let quit_id_clone = quit_id.clone(); tray_timer.start(TimerMode::Repeated, Duration::from_millis(100), move || { - if let (Ok(event), Some(app)) = (MenuEvent::receiver().try_recv(), weak.upgrade()) { - if event.id == show_id_clone { - let _ = app.show(); - } else if event.id == mini_id_clone { - let demo = app.get_active_endpoint_label().as_str() == "demo"; - if let Err(error) = spawn_mini_overlay(demo) { - app.set_error_text(error.into()); + if let Some(ids) = tray_ids.as_ref() { + if let (Ok(event), Some(app)) = (MenuEvent::receiver().try_recv(), weak.upgrade()) { + if event.id == ids.show { + let _ = app.show(); + } else if event.id == ids.mini { + let demo = app.get_active_endpoint_label().as_str() == "demo"; + if let Err(error) = spawn_mini_overlay(demo) { + app.set_error_text(error.into()); + } + } else if event.id == ids.quit { + let _ = slint::quit_event_loop(); } - } else if event.id == quit_id_clone { - let _ = slint::quit_event_loop(); } } if let (Ok(event), Some(app)) = (TrayIconEvent::receiver().try_recv(), weak.upgrade()) { diff --git a/src/cli/init.rs b/src/cli/init.rs index 5227702..85f4fe7 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,5 +1,6 @@ use crate as vimit; use std::io::{self, Write}; +use std::path::Path; use super::config::dirs_or_default; @@ -79,7 +80,7 @@ pub fn run_init() -> Result { let env_content = format!( "VIBEMODE_API_KEY={key}\n# VIBEMODE_API_BASE=https://r-api.vibemod.pro\n" ); - std::fs::write(&env_file, env_content) + write_secret_env_file(&env_file, env_content.as_bytes()) .map_err(|e| format!("cannot write .env: {e}"))?; println!(" .env created with VIBEMODE_API_KEY"); println!(" hint: you can also set the env var directly or use --api-key-env"); @@ -132,3 +133,60 @@ fn test_connection() -> Result<(), String> { } } } + +fn write_secret_env_file(path: &Path, content: &[u8]) -> std::io::Result<()> { + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + + let mut file = options.open(path)?; + file.write_all(content) +} + +#[cfg(test)] +mod tests { + use super::write_secret_env_file; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn secret_env_file_uses_create_new() { + let path = std::env::temp_dir().join(format!( + "vimit-init-create-new-{}.env", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + + write_secret_env_file(&path, b"VIBEMODE_API_KEY=test\n").unwrap(); + let err = write_secret_env_file(&path, b"VIBEMODE_API_KEY=other\n").unwrap_err(); + + assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists); + let _ = std::fs::remove_file(path); + } + + #[cfg(unix)] + #[test] + fn secret_env_file_is_owner_only_on_unix() { + use std::os::unix::fs::PermissionsExt; + + let path = std::env::temp_dir().join(format!( + "vimit-init-owner-only-{}.env", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + + write_secret_env_file(&path, b"VIBEMODE_API_KEY=test\n").unwrap(); + let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777; + + assert_eq!(mode, 0o600); + let _ = std::fs::remove_file(path); + } +}