focal 0.2.8

Terminal focus library - focus terminal windows and multiplexer panes
Documentation
//! Cross-platform window activation.
//!
//! Provides functions to bring a terminal window to the front on both
//! macOS and Linux.

use std::process::Command;

/// Activate (bring to front) a window by app name.
///
/// This is a cross-platform function that uses:
/// - macOS: osascript (AppleScript) with timeout protection
/// - Linux: wmctrl, xdotool, swaymsg, or hyprctl (tried in order)
///
/// # Arguments
/// * `app_name` - The application name to activate
///
/// # Returns
/// `true` if activation succeeded, `false` otherwise.
pub fn window(app_name: &str) -> bool {
    #[cfg(target_os = "macos")]
    {
        activate_macos(app_name)
    }

    #[cfg(target_os = "linux")]
    {
        activate_linux(app_name)
    }

    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    {
        let _ = app_name;
        false
    }
}

/// Activate a window on macOS using osascript with timeout protection.
///
/// AppleEvent default timeout is 60 seconds, which would freeze the app.
/// We use a 5-second timeout to fail fast.
#[cfg(target_os = "macos")]
fn activate_macos(app_name: &str) -> bool {
    use std::sync::mpsc;
    use std::thread;
    use std::time::Duration;

    const ACTIVATE_TIMEOUT: Duration = Duration::from_secs(5);

    let script = format!(r#"tell application "{app_name}" to activate"#);
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let result = Command::new("osascript")
            .args(["-e", &script])
            .status()
            .map(|s| s.success())
            .unwrap_or(false);
        let _ = tx.send(result);
    });

    rx.recv_timeout(ACTIVATE_TIMEOUT).unwrap_or(false)
}

/// Activate a window on Linux, trying multiple methods.
#[cfg(target_os = "linux")]
fn activate_linux(app_name: &str) -> bool {
    // Try wmctrl first (X11, most common)
    if Command::new("wmctrl")
        .args(["-a", app_name])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
    {
        return true;
    }

    // Try xdotool (X11 alternative)
    if Command::new("xdotool")
        .args(["search", "--name", app_name, "windowactivate"])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
    {
        return true;
    }

    // Try swaymsg (Sway/wlroots Wayland compositors)
    let sway_cmd = format!(r#"[app_id="{app_name}"] focus"#);
    if Command::new("swaymsg")
        .arg(&sway_cmd)
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
    {
        return true;
    }

    // Try hyprctl (Hyprland Wayland compositor)
    if Command::new("hyprctl")
        .args(["dispatch", "focuswindow", app_name])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
    {
        return true;
    }

    false
}

#[cfg(test)]
mod tests {
    // Note: These tests are limited since we can't easily test window activation
    // in CI environments. The main value is ensuring the code compiles on all
    // platforms.

    #[test]
    fn test_window_nonexistent_app() {
        // Activating a non-existent app should return false
        let result = super::window("definitely-not-a-real-app-12345");
        assert!(!result);
    }
}