use std::path::PathBuf; mod config; mod tray; fn exe_dir() -> PathBuf { std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.to_path_buf())) .unwrap_or_else(|| PathBuf::from("/usr/bin")) } fn find_sibling_binary(name: &str) -> String { let sibling = exe_dir().join(name); if sibling.exists() { return sibling.to_string_lossy().into_owned(); } name.to_string() } fn is_daemon_running() -> bool { std::process::Command::new("pgrep") .arg("-x") .arg("vietc") .status() .map(|s| s.success()) .unwrap_or(false) } fn needs_root() -> bool { let cfg = config::Config::load(); cfg.grab } /// Show a password prompt using available desktop tools. /// Returns the password, or empty string if cancelled. fn prompt_password() -> String { let title = "Viet+"; let msg = "Viet+ needs root privileges to capture keyboard input.\nPlease enter your password:"; // Try zenity (GNOME) if let Ok(output) = std::process::Command::new("zenity") .args(["--password", "--title", title, "--text", msg]) .stderr(std::process::Stdio::null()) .output() { if output.status.success() { let pw = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !pw.is_empty() { return pw; } } } // Try kdialog (KDE) if let Ok(output) = std::process::Command::new("kdialog") .args(["--password", msg]) .stderr(std::process::Stdio::null()) .output() { if output.status.success() { let pw = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !pw.is_empty() { return pw; } } } // Try ssh-askpass (X11 fallback) if let Ok(output) = std::process::Command::new("ssh-askpass") .arg(msg) .stderr(std::process::Stdio::null()) .output() { if output.status.success() { let pw = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !pw.is_empty() { return pw; } } } // Last resort: terminal prompt eprintln!("{}", msg); if let Ok(child) = std::process::Command::new("sh") .arg("-c") .arg("read -s -p 'Password: ' pw && echo \"$pw\"") .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::piped()) .spawn() { if let Ok(output) = child.wait_with_output() { if output.status.success() { return String::from_utf8_lossy(&output.stdout).trim().to_string(); } } } String::new() } fn start_daemon() { let daemon_bin = find_sibling_binary("vietc"); if needs_root() && !is_daemon_running() { // Mark that we've attempted first launch let flag_path = config_path().join(".first-launch-done"); if !flag_path.exists() { let password = prompt_password(); if password.is_empty() { eprintln!("[vietc-tray] No password provided, starting daemon without root"); let _ = std::process::Command::new(&daemon_bin).spawn(); return; } // Start daemon with sudo let mut child = match std::process::Command::new("sudo") .args(["-S", &daemon_bin]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() { Ok(c) => c, Err(e) => { eprintln!("[vietc-tray] Failed to start daemon with sudo: {}", e); let _ = std::process::Command::new(&daemon_bin).spawn(); return; } }; if let Some(mut stdin) = child.stdin.take() { use std::io::Write; let _ = stdin.write_all(format!("{}\n", password).as_bytes()); } let _ = child.wait(); // Mark first launch as done let _ = std::fs::write(&flag_path, "1"); return; } } if !is_daemon_running() { eprintln!("[vietc-tray] Starting daemon: {}", daemon_bin); let _ = std::process::Command::new(&daemon_bin).spawn(); } } fn config_path() -> PathBuf { dirs::config_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("vietc") } fn main() { eprintln!("[vietc-tray] Starting"); // Start daemon (with password prompt if first launch) start_daemon(); // Run the tray tray::run(); }