focal 0.2.8

Terminal focus library - focus terminal windows and multiplexer panes
Documentation
//! JavaScript for Automation (JXA) utilities for macOS.
//!
//! Provides timeout-protected execution of JXA scripts to prevent
//! indefinite blocking on AppleEvent timeouts.

#[cfg(target_os = "macos")]
use crate::util::DEFAULT_TIMEOUT;

/// Run a JavaScript for Automation (JXA) script with timeout protection.
///
/// Returns `true` if the script executed successfully and returned "true",
/// `false` otherwise (including timeout).
///
/// # Arguments
/// * `script` - The JXA script to execute
#[cfg(target_os = "macos")]
pub fn run_with_timeout(script: &str) -> bool {
    use std::sync::mpsc;
    use std::thread;

    let script_owned = script.to_string();
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let output = std::process::Command::new("osascript")
            .args(["-l", "JavaScript", "-e", &script_owned])
            .output();
        let _ = tx.send(output);
    });

    match rx.recv_timeout(DEFAULT_TIMEOUT) {
        Ok(Ok(out)) if out.status.success() => {
            String::from_utf8_lossy(&out.stdout).trim() == "true"
        }
        _ => false,
    }
}

/// Raise the frontmost window of an app using System Events AXRaise.
///
/// This brings only the frontmost window of the specified app to the front,
/// without disturbing other windows. Requires accessibility permissions on macOS.
///
/// # Arguments
/// * `app_name` - The application name as it appears in System Events (e.g., "Ghostty", "kitty", "WezTerm")
///
/// # Returns
/// `true` if the window was successfully raised, `false` otherwise.
#[cfg(target_os = "macos")]
pub fn raise_front_window(app_name: &str) -> bool {
    let script = format!(
        r#"
(function() {{
    try {{
        var se = Application("System Events");
        var proc = se.processes["{app_name}"];
        var seWindows = proc.windows();
        if (seWindows.length > 0) {{
            seWindows[0].actions["AXRaise"].perform();
            return true;
        }}
        return false;
    }} catch(e) {{
        return false;
    }}
}})()
"#
    );

    run_with_timeout(&script)
}

/// Stub for non-macOS platforms - AXRaise is not available.
#[cfg(not(target_os = "macos"))]
pub fn raise_front_window(_app_name: &str) -> bool {
    false
}

/// Raise the focused window of an app using System Events AXFocusedWindow + AXRaise.
///
/// Unlike [`raise_front_window`], this uses the `AXFocusedWindow` attribute to
/// identify the window that the app considers focused. This is more reliable when
/// the app has multiple windows and the focused window is not the frontmost one
/// in System Events' window list.
///
/// Falls back to `raise_front_window` behavior if `AXFocusedWindow` is not available.
///
/// # Arguments
/// * `app_name` - The application name as it appears in System Events
///
/// # Returns
/// `true` if the window was successfully raised, `false` otherwise.
#[cfg(target_os = "macos")]
pub fn raise_focused_window(app_name: &str) -> bool {
    let script = format!(
        r#"
(function() {{
    try {{
        var se = Application("System Events");
        var proc = se.processes["{app_name}"];

        // Try to get the focused window first
        var focusedWindow = proc.attributes["AXFocusedWindow"].value();
        if (focusedWindow) {{
            focusedWindow.actions["AXRaise"].perform();
            return true;
        }}

        // Fallback: if AXFocusedWindow is not available, try first window
        var seWindows = proc.windows();
        if (seWindows.length > 0) {{
            seWindows[0].actions["AXRaise"].perform();
            return true;
        }}
        return false;
    }} catch(e) {{
        return false;
    }}
}})()
"#
    );

    run_with_timeout(&script)
}

/// Stub for non-macOS platforms - AXFocusedWindow is not available.
#[cfg(not(target_os = "macos"))]
#[cfg_attr(not(test), expect(dead_code))]
pub fn raise_focused_window(_app_name: &str) -> bool {
    false
}

/// Activate an application, bringing all its windows to the front.
///
/// This first checks if the app is running to avoid launching it when
/// it's not already open. This is the "ActivateApp" focus mode behavior.
///
/// # Arguments
/// * `app_name` - The application name (e.g., "Ghostty", "Warp", "Wave")
///
/// # Returns
/// `true` if the app was successfully activated, `false` otherwise.
#[cfg(target_os = "macos")]
pub fn activate_app(app_name: &str) -> bool {
    let script = format!(
        r#"
(function() {{
    try {{
        var app = Application("{app_name}");
        if (!app.running()) return false;
        app.activate();
        return true;
    }} catch(e) {{
        return false;
    }}
}})()
"#
    );

    run_with_timeout(&script)
}

/// Stub for non-macOS platforms - JXA activation is not available.
#[cfg(not(target_os = "macos"))]
pub fn activate_app(_app_name: &str) -> bool {
    false
}

/// Stub for non-macOS platforms - JXA is not available.
#[cfg(not(target_os = "macos"))]
#[cfg_attr(not(test), expect(dead_code))]
pub fn run_with_timeout(_script: &str) -> bool {
    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_run_with_timeout_simple_script() {
        // A simple script that returns true
        let result = run_with_timeout("true");
        #[cfg(target_os = "macos")]
        assert!(result);
        #[cfg(not(target_os = "macos"))]
        assert!(!result);
    }

    #[test]
    fn test_run_with_timeout_false_script() {
        // A script that returns false
        let result = run_with_timeout("false");
        assert!(!result);
    }

    #[test]
    fn test_raise_front_window_nonexistent_app() {
        // Raising a window for an app that doesn't exist should return false
        let result = raise_front_window("NonExistentApp12345");
        assert!(!result);
    }

    #[test]
    fn test_raise_focused_window_nonexistent_app() {
        // Raising a focused window for an app that doesn't exist should return false
        let result = raise_focused_window("NonExistentApp12345");
        assert!(!result);
    }

    #[test]
    fn test_activate_app_nonexistent() {
        // Activating a non-existent app should return false
        let result = activate_app("NonExistentApp12345");
        assert!(!result);
    }
}