nex-launch 2.1.0

A keyboard-first launcher for Windows
use crate::action_registry::{
    ACTION_CHECK_UPDATES_ID, ACTION_CLEAR_CLIPBOARD_ID, ACTION_DIAGNOSTICS_BUNDLE_ID,
    ACTION_OPEN_CONFIG_ID, ACTION_OPEN_LOGS_ID, ACTION_REBUILD_INDEX_ID, ACTION_TRIM_MEMORY_ID,
    ACTION_WEB_SEARCH_PREFIX,
};
use crate::clipboard_history;
use crate::config::Config;
use crate::core_service::{CoreService, LaunchTarget};
use crate::model::SearchItem;
use crate::plugin_sdk::{PluginActionKind, PluginRegistry};
use crate::query_dsl::ParsedQuery;
use crate::runtime::log_info;
use crate::runtime_overlay_rows::{
    uninstall_target_title_from_action_title, ACTION_UNINSTALL_CANCEL_ID,
    ACTION_UNINSTALL_CONFIRM_ID,
};
use crate::runtime_search_session::resolved_mode_for_query;

pub(crate) fn should_suppress_failed_uninstall(error: &str) -> bool {
    let lower = error.to_ascii_lowercase();
    lower.contains("shell_code=2")
        || lower.contains(" code 2")
        || lower.contains("no longer available")
        || lower.contains("file not found")
}

#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
pub(crate) fn uninstall_confirmation_results(uninstall_action: &SearchItem) -> Vec<SearchItem> {
    let target = uninstall_target_title_from_action_title(uninstall_action.title.as_str())
        .unwrap_or_else(|| uninstall_action.title.trim().to_string());
    let confirm_title = if target.is_empty() {
        "Confirm uninstall".to_string()
    } else {
        format!("Confirm uninstall {}", target.trim())
    };

    vec![
        SearchItem::new(
            ACTION_UNINSTALL_CONFIRM_ID,
            "action",
            confirm_title.as_str(),
            "Open app uninstaller",
        ),
        SearchItem::new(
            ACTION_UNINSTALL_CANCEL_ID,
            "action",
            "Cancel",
            "Return to previous results",
        ),
    ]
}

#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn launch_overlay_selection(
    service: &CoreService,
    cfg: &Config,
    plugins: &PluginRegistry,
    results: &[SearchItem],
    selected_index: usize,
    query_text: &str,
) -> Result<(), String> {
    if results.is_empty() {
        return Err("no result selected".to_string());
    }

    if selected_index >= results.len() {
        return Err(format!(
            "selected index out of range: {selected_index} (len={})",
            results.len()
        ));
    }

    let selected = &results[selected_index];
    if selected.kind.eq_ignore_ascii_case("action") {
        return execute_action_selection(service, cfg, plugins, selected);
    }
    if selected.kind.eq_ignore_ascii_case("clipboard") {
        return clipboard_history::copy_result_to_clipboard(cfg, &selected.id);
    }

    let parsed_query = ParsedQuery::parse(query_text.trim(), cfg.search_dsl_enabled);
    let mode = resolved_mode_for_query(cfg, &parsed_query);
    service
        .launch_with_query_context(LaunchTarget::Id(&selected.id), Some(query_text), Some(mode))
        .map_err(|error| format!("launch failed: {error}"))
}

#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn execute_action_selection(
    service: &CoreService,
    cfg: &Config,
    plugins: &PluginRegistry,
    selected: &SearchItem,
) -> Result<(), String> {
    if selected
        .id
        .starts_with(crate::uninstall_registry::ACTION_UNINSTALL_PREFIX)
    {
        return crate::uninstall_registry::execute_uninstall_action(&selected.id)
            .map_err(|error| format!("uninstall launch failed: {error}"));
    }

    if selected.id.starts_with(ACTION_WEB_SEARCH_PREFIX) {
        return crate::action_executor::launch_open_target(selected.path.trim())
            .map_err(|error| format!("web search launch failed: {error}"));
    }

    match selected.id.as_str() {
        ACTION_OPEN_LOGS_ID => crate::logging::open_logs_folder()
            .map_err(|error| format!("open logs folder failed: {error}")),
        ACTION_REBUILD_INDEX_ID => {
            let report = service
                .rebuild_index_with_report()
                .map_err(|error| format!("rebuild index failed: {error}"))?;
            log_info(&format!(
                "[nex] action_rebuild_index indexed={} discovered={} upserted={} removed={}",
                report.indexed_total,
                report.discovered_total,
                report.upserted_total,
                report.removed_total
            ));
            Ok(())
        }
        ACTION_CLEAR_CLIPBOARD_ID => clipboard_history::clear_history(cfg),
        ACTION_OPEN_CONFIG_ID => {
            crate::action_executor::launch_path(cfg.config_path.to_string_lossy().as_ref())
                .map_err(|error| format!("open config failed: {error}"))
        }
        ACTION_DIAGNOSTICS_BUNDLE_ID => {
            let output_dir = crate::runtime::write_diagnostics_bundle(cfg)
                .map_err(|error| format!("diagnostics bundle failed: {error}"))?;
            log_info(&format!(
                "[nex] diagnostics bundle written to {}",
                output_dir.display()
            ));
            Ok(())
        }
        ACTION_CHECK_UPDATES_ID => crate::runtime_process::launch_stable_updater()
            .map(|_| ())
            .map_err(|error| format!("check for updates failed: {error}")),
        ACTION_TRIM_MEMORY_ID => {
            log_info("[nex] trim memory action invoked");
            Ok(())
        }
        _ => execute_plugin_action(cfg, plugins, &selected.id),
    }
}

pub(crate) fn execute_plugin_action(
    cfg: &Config,
    plugins: &PluginRegistry,
    result_id: &str,
) -> Result<(), String> {
    let action = plugins
        .actions_by_result_id
        .get(result_id)
        .ok_or_else(|| "unknown action".to_string())?;

    match &action.kind {
        PluginActionKind::OpenPath { path } => crate::action_executor::launch_path(path)
            .map_err(|error| format!("plugin open path failed: {error}")),
        PluginActionKind::Command { command, args } => {
            if cfg.plugins_safe_mode {
                return Err(
                    "plugin command execution blocked: plugins_safe_mode is enabled in config"
                        .to_string(),
                );
            }
            if command.trim().is_empty() {
                return Err("plugin command action missing command".to_string());
            }
            std::process::Command::new(command)
                .args(args)
                .spawn()
                .map_err(|e| format!("plugin command spawn failed: {e}"))?;
            Ok(())
        }
    }
}