use crate::action_registry::{
search_actions_with_mode, 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::{self, Config, ConfigError};
use crate::core_service::{CoreService, LaunchTarget, ServiceError};
use crate::hotkey_runtime::HotkeyRuntimeError;
#[cfg(target_os = "windows")]
use crate::hotkey_runtime::{default_hotkey_registrar, HotkeyRegistration};
#[cfg(target_os = "windows")]
use crate::overlay_state::{HotkeyAction, OverlayState};
use crate::plugin_sdk::{PluginActionKind, PluginRegistry};
use crate::query_dsl::ParsedQuery;
use crate::search::SearchFilter;
#[cfg(target_os = "windows")]
use crate::windows_overlay::{
is_instance_window_present, signal_existing_instance_quit, signal_existing_instance_show,
NativeOverlayShell, OverlayEvent, OverlayRow, OverlayRowRole,
};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "windows")]
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[cfg(target_os = "windows")]
use std::time::{Duration, SystemTime};
#[cfg(target_os = "windows")]
const STATUS_ROW_NO_RESULTS: &str = "No results";
#[cfg(target_os = "windows")]
const STATUS_ROW_NO_COMMAND_RESULTS: &str = "No command matches";
#[cfg(target_os = "windows")]
const STATUS_ROW_TYPE_TO_SEARCH: &str = "Start typing to search";
#[cfg(target_os = "windows")]
const STATUS_ROW_INDEXING: &str = "Indexing in background...";
#[cfg(target_os = "windows")]
const STATUS_TEXT_INDEX_READY: &str = "Index ready";
const QUERY_PROFILE_LOG_THRESHOLD_MS: u128 = 35;
const SHORT_QUERY_APP_BIAS_MAX_LEN: usize = 2;
const INDEXED_PREFIX_CACHE_MIN_QUERY_LEN: usize = 1;
const INDEXED_PREFIX_CACHE_MIN_SEED_LIMIT: usize = 120;
const INDEXED_PREFIX_CACHE_MAX_SEED_LIMIT: usize = 480;
const QUERY_PROFILE_STATUS_SAMPLE_WINDOW: usize = 400;
const FINAL_QUERY_CACHE_MAX_ENTRIES: usize = 32;
const ADAPTIVE_INDEXED_LATENCY_WINDOW: usize = 24;
#[cfg(target_os = "windows")]
const QUEUED_DISCOVERY_REINDEX_DEBOUNCE_MS: u64 = 1200;
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
const UNINSTALL_QUERY_RESULT_LIMIT: usize = 160;
#[cfg(target_os = "windows")]
const CONFIG_RELOAD_POLL_INTERVAL: Duration = Duration::from_millis(500);
const ACTION_UNINSTALL_CONFIRM_ID: &str = "action:uninstall:confirm";
const ACTION_UNINSTALL_CANCEL_ID: &str = "action:uninstall:cancel";
static STDIO_LOGGING_ENABLED: AtomicBool = AtomicBool::new(true);
#[cfg(target_os = "windows")]
const CURRENT_RUNTIME_EXE_NAME: &str = "nex.exe";
#[cfg(target_os = "windows")]
const LEGACY_RUNTIME_EXE_NAMES: &[&str] = &["nex-core.exe", "swiftfind-core.exe"];
const CURRENT_LOG_PREFIX: &str = "[nex]";
const LEGACY_LOG_PREFIXES: &[&str] = &["[nex-core]", "[swiftfind-core]"];
fn env_var_with_legacy(current: &str, legacy: &str) -> Result<String, std::env::VarError> {
std::env::var(current).or_else(|_| std::env::var(legacy))
}
#[cfg(target_os = "windows")]
fn runtime_executable_names() -> impl Iterator<Item = &'static str> {
std::iter::once(CURRENT_RUNTIME_EXE_NAME).chain(LEGACY_RUNTIME_EXE_NAMES.iter().copied())
}
fn runtime_log_prefixes() -> impl Iterator<Item = &'static str> {
std::iter::once(CURRENT_LOG_PREFIX).chain(LEGACY_LOG_PREFIXES.iter().copied())
}
fn runtime_log_marker(prefix: &str, marker: &str) -> String {
format!("{prefix} {marker}")
}
fn rfind_runtime_log_marker(content: &str, marker: &str) -> Option<usize> {
runtime_log_prefixes()
.filter_map(|prefix| content.rfind(&runtime_log_marker(prefix, marker)))
.max()
}
fn line_contains_runtime_log_marker(line: &str, marker: &str) -> bool {
runtime_log_prefixes().any(|prefix| line.contains(&runtime_log_marker(prefix, marker)))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn hotkey_registration_recovery_message(hotkey: &str, config_path: &std::path::Path) -> String {
let suggestions = crate::settings::suggested_hotkey_presets(hotkey, 3);
if suggestions.is_empty() {
return format!(
"Hotkey '{hotkey}' is unavailable. Open {} and choose a different modifier+key combination.",
config_path.display()
);
}
format!(
"Hotkey '{hotkey}' is unavailable. Try {}. Edit {} to change it.",
suggestions.join(", "),
config_path.display()
)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn hotkey_registration_status_text(hotkey: &str) -> String {
let suggestions = crate::settings::suggested_hotkey_presets(hotkey, 2);
if suggestions.is_empty() {
return format!("Hotkey unavailable: {hotkey}. Open config from the tray.");
}
format!(
"Hotkey unavailable: {hotkey}. Try {}.",
suggestions.join(" or ")
)
}
fn launch_stable_updater() -> Result<std::path::PathBuf, String> {
let script_path = crate::updater::launch_updater(crate::updater::UpdateChannel::Stable)
.map_err(|error| error.to_string())?;
log_info(&format!(
"[nex] updater_launch channel=stable script={}",
script_path.display()
));
Ok(script_path)
}
#[derive(Debug, Clone, Default)]
struct OverlaySearchSession {
indexed_prefix_cache: Option<IndexedPrefixCache>,
final_query_cache: HashMap<String, Vec<crate::model::SearchItem>>,
final_query_cache_lru: VecDeque<String>,
indexed_latency_ms: VecDeque<u128>,
}
impl OverlaySearchSession {
fn clear(&mut self) {
self.indexed_prefix_cache = None;
self.final_query_cache.clear();
self.final_query_cache_lru.clear();
self.indexed_latency_ms.clear();
}
}
#[derive(Debug, Clone)]
struct IndexedPrefixCache {
normalized_query: String,
indexed_filter: SearchFilter,
seed_items: Vec<crate::model::SearchItem>,
}
#[cfg(target_os = "windows")]
#[derive(Debug, Clone)]
struct PendingUninstallConfirmation {
uninstall_action: crate::model::SearchItem,
previous_results: Vec<crate::model::SearchItem>,
previous_selected_index: usize,
previous_command_mode: bool,
}
#[cfg(target_os = "windows")]
#[derive(Debug)]
struct RuntimeConfigWatcher {
path: std::path::PathBuf,
last_checked: Instant,
last_modified: Option<SystemTime>,
}
#[cfg(target_os = "windows")]
#[derive(Debug)]
struct BackgroundIndexRefresh {
completed: Arc<AtomicBool>,
result: Arc<Mutex<Option<Result<crate::core_service::IndexRefreshReport, String>>>>,
cache_applied: bool,
initial_cache_empty: bool,
pending_discovery_reindex: bool,
pending_discovery_reindex_due_at: Option<Instant>,
pending_discovery_reindex_requests: usize,
ready_notice_pending: bool,
started_at: Instant,
startup_started_at: Instant,
}
#[derive(Debug)]
pub enum RuntimeError {
Args(String),
Config(ConfigError),
Service(ServiceError),
Hotkey(HotkeyRuntimeError),
Overlay(String),
Startup(crate::startup::StartupError),
Io(std::io::Error),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Args(error) => write!(f, "argument error: {error}"),
Self::Config(error) => write!(f, "config error: {error}"),
Self::Service(error) => write!(f, "service error: {error}"),
Self::Hotkey(error) => write!(f, "hotkey runtime error: {error:?}"),
Self::Overlay(error) => write!(f, "overlay error: {error}"),
Self::Startup(error) => write!(f, "startup error: {error}"),
Self::Io(error) => write!(f, "io error: {error}"),
}
}
}
impl std::error::Error for RuntimeError {}
impl From<ConfigError> for RuntimeError {
fn from(value: ConfigError) -> Self {
Self::Config(value)
}
}
impl From<ServiceError> for RuntimeError {
fn from(value: ServiceError) -> Self {
Self::Service(value)
}
}
impl From<HotkeyRuntimeError> for RuntimeError {
fn from(value: HotkeyRuntimeError) -> Self {
Self::Hotkey(value)
}
}
impl From<crate::startup::StartupError> for RuntimeError {
fn from(value: crate::startup::StartupError) -> Self {
Self::Startup(value)
}
}
impl From<std::io::Error> for RuntimeError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeCommand {
Run,
Status,
StatusJson,
Quit,
Restart,
EnsureConfig,
SyncStartup,
SetLaunchAtStartup(bool),
DiagnosticsBundle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RuntimeOptions {
pub command: RuntimeCommand,
pub background: bool,
}
impl Default for RuntimeOptions {
fn default() -> Self {
Self {
command: RuntimeCommand::Run,
background: false,
}
}
}
pub fn parse_cli_args(args: &[String]) -> Result<RuntimeOptions, String> {
let mut options = RuntimeOptions::default();
for arg in args {
if let Some(value) = arg.strip_prefix("--set-launch-at-startup=") {
let enabled = match value.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => true,
"false" | "0" | "no" | "off" => false,
_ => {
return Err(format!(
"invalid value for --set-launch-at-startup: {value} (expected true/false)"
));
}
};
options.command = RuntimeCommand::SetLaunchAtStartup(enabled);
continue;
}
match arg.as_str() {
"--background" => options.background = true,
"--foreground" => options.background = false,
"--status" => options.command = RuntimeCommand::Status,
"--status-json" => options.command = RuntimeCommand::StatusJson,
"--quit" => options.command = RuntimeCommand::Quit,
"--restart" => options.command = RuntimeCommand::Restart,
"--ensure-config" => options.command = RuntimeCommand::EnsureConfig,
"--sync-startup" => options.command = RuntimeCommand::SyncStartup,
"--diagnostics-bundle" => options.command = RuntimeCommand::DiagnosticsBundle,
"--help" | "-h" => {
return Err(
"usage: nex [--background|--foreground] [--status|--status-json|--quit|--restart|--ensure-config|--sync-startup|--set-launch-at-startup=true|false|--diagnostics-bundle]".to_string(),
)
}
unknown => return Err(format!("unknown argument: {unknown}")),
}
}
if options.command != RuntimeCommand::Run && options.background {
return Err("background mode is only valid with normal run mode".to_string());
}
Ok(options)
}
pub fn run() -> Result<(), RuntimeError> {
run_with_options(RuntimeOptions::default())
}
pub fn run_with_options(options: RuntimeOptions) -> Result<(), RuntimeError> {
configure_stdio_logging(options);
if let Err(error) = crate::logging::init() {
log_warn(&format!("[nex] logging init warning: {error}"));
}
#[cfg(target_os = "windows")]
if options.background && options.command == RuntimeCommand::Run {
return spawn_background_process();
}
match options.command {
RuntimeCommand::Status => return command_status(),
RuntimeCommand::StatusJson => return command_status_json(),
RuntimeCommand::Quit => return command_quit(),
RuntimeCommand::Restart => return command_restart(),
RuntimeCommand::EnsureConfig => return command_ensure_config(),
RuntimeCommand::SyncStartup => return command_sync_startup(),
RuntimeCommand::SetLaunchAtStartup(enabled) => {
return command_set_launch_at_startup(enabled);
}
RuntimeCommand::DiagnosticsBundle => return command_diagnostics_bundle(),
RuntimeCommand::Run => {}
}
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
let startup_started_at = Instant::now();
#[cfg_attr(not(target_os = "windows"), allow(unused_mut))]
let mut runtime_config = config::load(None)?;
if !runtime_config.config_path.exists() {
config::write_user_template(&runtime_config, &runtime_config.config_path)?;
log_info(&format!(
"[nex] wrote user config template to {}",
runtime_config.config_path.display()
));
}
log_info(&format!(
"[nex] startup mode={} hotkey={} config_path={} index_db_path={}",
runtime_mode(),
runtime_config.hotkey,
runtime_config.config_path.display(),
runtime_config.index_db_path.display(),
));
let service = CoreService::new(runtime_config.clone())?.with_runtime_providers();
#[cfg(target_os = "windows")]
let mut background_index_refresh = {
let initial_cached_items = service.cached_items_len();
log_info(&format!(
"[nex] startup cached_items={} (async indexing scheduled)",
initial_cached_items
));
log_info(&format!(
"[nex] startup_phase phase=indexing_started elapsed_ms={} initial_cache_empty={} cached_items={}",
startup_started_at.elapsed().as_millis(),
initial_cached_items == 0,
initial_cached_items
));
start_background_index_refresh(
&runtime_config,
initial_cached_items == 0,
startup_started_at,
)
};
#[cfg(not(target_os = "windows"))]
{
let index_report = service.rebuild_index_incremental_with_report()?;
log_info(&format!(
"[nex] startup indexed_items={} discovered={} upserted={} removed={}",
index_report.indexed_total,
index_report.discovered_total,
index_report.upserted_total,
index_report.removed_total,
));
for provider in &index_report.providers {
log_info(&format!(
"[nex] index_provider name={} discovered={} upserted={} removed={} skipped={} elapsed_ms={}",
provider.provider,
provider.discovered,
provider.upserted,
provider.removed,
provider.skipped,
provider.elapsed_ms,
));
}
}
#[cfg(target_os = "windows")]
let mut plugin_registry = PluginRegistry::load_from_config(&runtime_config);
#[cfg(target_os = "windows")]
{
for warning in &plugin_registry.load_warnings {
log_warn(&format!("[nex] plugin_warning {warning}"));
}
log_info(&format!(
"[nex] plugins loaded provider_items={} action_items={}",
plugin_registry.provider_items.len(),
plugin_registry.action_items.len()
));
}
#[cfg(target_os = "windows")]
{
unsafe {
let _ = windows_sys::Win32::UI::HiDpi::SetProcessDpiAwarenessContext(
windows_sys::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
);
}
if let Ok(exe) = std::env::current_exe() {
if let Err(error) = crate::startup::set_enabled(runtime_config.launch_at_startup, &exe)
{
log_warn(&format!("[nex] startup sync warning: {error}"));
}
}
let _single_instance = match acquire_single_instance_guard() {
Ok(guard) => guard,
Err(error) => return Err(RuntimeError::Overlay(error)),
};
if _single_instance.is_none() {
let _ = signal_existing_instance_show();
log_info("[nex] runtime already active; signaled existing instance");
return Ok(());
}
let mut overlay_state = OverlayState::default();
let overlay = NativeOverlayShell::create().map_err(RuntimeError::Overlay)?;
overlay.set_help_config_path(runtime_config.config_path.to_string_lossy().as_ref());
overlay.set_hotkey_hint(&runtime_config.hotkey);
overlay.set_performance_tuning(
runtime_config.idle_cache_trim_ms,
runtime_config.active_memory_target_mb,
);
overlay.set_game_mode_enabled(runtime_config.game_mode_enabled);
log_info("[nex] native overlay shell initialized (hidden)");
log_info(&format!(
"[nex] startup_phase phase=overlay_ready elapsed_ms={}",
startup_started_at.elapsed().as_millis()
));
let mut registrar = default_hotkey_registrar();
let hotkey_issue_status = match registrar.register_hotkey(&runtime_config.hotkey) {
Ok(registration) => {
log_registration(®istration);
overlay.set_hotkey_issue_active(false);
None
}
Err(error) => {
let recovery_message = hotkey_registration_recovery_message(
&runtime_config.hotkey,
&runtime_config.config_path,
);
let suggested = crate::settings::suggested_hotkey_presets(
&runtime_config.hotkey,
3,
)
.join("|");
log_warn(&format!(
"[nex] hotkey_registration_issue hotkey={} suggestions={} error={:?}",
runtime_config.hotkey, suggested, error
));
log_warn(&format!("[nex] {recovery_message}"));
overlay.set_hotkey_issue_active(true);
Some(hotkey_registration_status_text(&runtime_config.hotkey))
}
};
log_info(&format!(
"[nex] startup_phase phase=hotkey_ready elapsed_ms={} hotkey={}",
startup_started_at.elapsed().as_millis(),
runtime_config.hotkey
));
log_info("[nex] event loop running (native overlay)");
let mut max_results = runtime_config.max_results as usize;
let mut config_watcher = RuntimeConfigWatcher {
path: runtime_config.config_path.clone(),
last_checked: Instant::now(),
last_modified: config_file_modified_time(runtime_config.config_path.as_path()),
};
let mut current_results: Vec<crate::model::SearchItem> = Vec::new();
let mut suppressed_uninstall_titles: Vec<String> = Vec::new();
let mut pending_uninstall_confirmation: Option<PendingUninstallConfirmation> = None;
let mut pending_delayed_query: Option<String> = None;
let mut selected_index = 0_usize;
let mut last_query = String::new();
let mut search_session = OverlaySearchSession::default();
overlay
.run_message_loop_with_events(|event| {
maybe_apply_runtime_config_reload(
&overlay,
&service,
&mut runtime_config,
&mut plugin_registry,
&mut search_session,
&mut pending_uninstall_confirmation,
&mut max_results,
&mut config_watcher,
&mut background_index_refresh,
);
maybe_apply_background_index_refresh(
&service,
&mut background_index_refresh,
&runtime_config,
);
maybe_show_background_index_ready_notice(&overlay, &mut background_index_refresh);
match event {
OverlayEvent::Hotkey(_) => {
log_info("[nex] hotkey_event received");
let overlay_visible = overlay.is_visible();
overlay_state.set_visible(overlay_visible);
if !overlay_visible
&& should_suppress_hotkey_for_game_mode(&runtime_config)
{
log_info(
"[nex] hotkey ignored because game mode is active for the foreground app",
);
return;
}
let action = overlay_state.on_hotkey(overlay.has_focus());
match action {
HotkeyAction::ShowAndFocus | HotkeyAction::FocusExisting => {
reconcile_suppressed_uninstall_titles(
&mut suppressed_uninstall_titles,
);
overlay.show_and_focus();
if runtime_config.clipboard_enabled {
let _ =
clipboard_history::maybe_capture_latest(&runtime_config);
}
if overlay.query_text().trim().is_empty() {
set_idle_overlay_state(&overlay);
if let Some(issue) = hotkey_issue_status.as_deref() {
overlay.set_status_text(issue);
}
maybe_show_background_index_ready_notice(
&overlay,
&mut background_index_refresh,
);
}
}
HotkeyAction::Hide => {
pending_delayed_query = None;
overlay.cancel_query_delay();
overlay.hide();
reset_overlay_session(
&overlay,
&mut current_results,
&mut selected_index,
);
pending_uninstall_confirmation = None;
last_query.clear();
search_session.clear();
maybe_apply_background_index_refresh(
&service,
&mut background_index_refresh,
&runtime_config,
);
}
}
}
OverlayEvent::ExternalShow => {
overlay_state.set_visible(overlay.is_visible());
reconcile_suppressed_uninstall_titles(&mut suppressed_uninstall_titles);
overlay.show_and_focus();
if runtime_config.clipboard_enabled {
let _ = clipboard_history::maybe_capture_latest(&runtime_config);
}
if overlay.query_text().trim().is_empty() {
set_idle_overlay_state(&overlay);
if let Some(issue) = hotkey_issue_status.as_deref() {
overlay.set_status_text(issue);
}
maybe_show_background_index_ready_notice(
&overlay,
&mut background_index_refresh,
);
}
}
OverlayEvent::ExternalQuit => {
pending_delayed_query = None;
overlay.cancel_query_delay();
overlay.hide_now();
last_query.clear();
search_session.clear();
unsafe {
windows_sys::Win32::UI::WindowsAndMessaging::PostQuitMessage(0);
}
}
OverlayEvent::TrayToggleGameMode => {
toggle_game_mode_from_tray(&overlay, &mut runtime_config);
}
OverlayEvent::TrayCheckForUpdates => {
match launch_stable_updater() {
Ok(_) => overlay.set_status_text("Updater launched"),
Err(error) => {
log_warn(&format!(
"[nex] updater launch failed from tray: {error}"
));
overlay.set_status_text("Could not launch updater");
}
}
}
OverlayEvent::Escape => {
if overlay_state.on_escape() {
pending_delayed_query = None;
overlay.cancel_query_delay();
overlay.hide_now();
reset_overlay_session(
&overlay,
&mut current_results,
&mut selected_index,
);
pending_uninstall_confirmation = None;
last_query.clear();
search_session.clear();
}
}
OverlayEvent::QueryChanged(query) => {
if runtime_config.search_query_results_with_delay {
pending_delayed_query = Some(query);
overlay
.schedule_query_delay(runtime_config.search_delay_time_ms as u32);
} else {
pending_delayed_query = None;
overlay.cancel_query_delay();
apply_query_change(
query,
&overlay,
&service,
&runtime_config,
&plugin_registry,
max_results,
&background_index_refresh,
&mut search_session,
&mut pending_uninstall_confirmation,
&suppressed_uninstall_titles,
&mut current_results,
&mut selected_index,
&mut last_query,
);
}
}
OverlayEvent::QueryDelayElapsed => {
if let Some(query) = pending_delayed_query.take() {
apply_query_change(
query,
&overlay,
&service,
&runtime_config,
&plugin_registry,
max_results,
&background_index_refresh,
&mut search_session,
&mut pending_uninstall_confirmation,
&suppressed_uninstall_titles,
&mut current_results,
&mut selected_index,
&mut last_query,
);
}
}
OverlayEvent::MoveSelection(direction) => {
if current_results.is_empty() {
return;
}
selected_index =
next_selection_index(selected_index, current_results.len(), direction);
overlay.set_selected_index(selected_index);
}
OverlayEvent::Submit => {
if let Some(query) = pending_delayed_query.take() {
overlay.cancel_query_delay();
apply_query_change(
query,
&overlay,
&service,
&runtime_config,
&plugin_registry,
max_results,
&background_index_refresh,
&mut search_session,
&mut pending_uninstall_confirmation,
&suppressed_uninstall_titles,
&mut current_results,
&mut selected_index,
&mut last_query,
);
}
if current_results.is_empty() {
if overlay.query_text().trim().is_empty() {
set_idle_overlay_state(&overlay);
overlay.show_placeholder_hint(STATUS_ROW_TYPE_TO_SEARCH);
} else if should_show_indexing_status(&background_index_refresh) {
set_status_row_overlay_state(&overlay, STATUS_ROW_INDEXING);
} else {
let parsed_query = ParsedQuery::parse(
overlay.query_text().trim(),
runtime_config.search_dsl_enabled,
);
set_status_row_overlay_state(
&overlay,
if parsed_query.command_mode {
STATUS_ROW_NO_COMMAND_RESULTS
} else {
STATUS_ROW_NO_RESULTS
},
);
}
return;
}
if let Some(list_selection) = overlay.selected_index() {
selected_index = list_selection.min(current_results.len() - 1);
}
let selected = ¤t_results[selected_index];
if pending_uninstall_confirmation.is_some() {
let selected_id = selected.id.clone();
if selected_id == ACTION_UNINSTALL_CONFIRM_ID {
let Some(pending) = pending_uninstall_confirmation.take() else {
return;
};
overlay.hide_now();
overlay_state.on_escape();
match execute_action_selection(
&service,
&runtime_config,
&plugin_registry,
&pending.uninstall_action,
) {
Ok(()) => {
track_uninstall_title_suppression(
&mut suppressed_uninstall_titles,
pending.uninstall_action.title.as_str(),
);
overlay.set_status_text("");
reset_overlay_session(
&overlay,
&mut current_results,
&mut selected_index,
);
last_query.clear();
search_session.clear();
}
Err(error) => {
if should_suppress_failed_uninstall(error.as_str()) {
track_uninstall_title_suppression(
&mut suppressed_uninstall_titles,
pending.uninstall_action.title.as_str(),
);
current_results = pending.previous_results;
filter_suppressed_uninstall_results(
&mut current_results,
&suppressed_uninstall_titles,
);
selected_index = pending
.previous_selected_index
.min(current_results.len().saturating_sub(1));
if current_results.is_empty() {
set_status_row_overlay_state(
&overlay,
if pending.previous_command_mode {
STATUS_ROW_NO_COMMAND_RESULTS
} else {
STATUS_ROW_NO_RESULTS
},
);
} else {
let rows = overlay_rows(
¤t_results,
pending.previous_command_mode,
);
overlay.set_results(&rows, selected_index);
}
overlay.set_status_text(
"Uninstall entry is stale and was hidden",
);
} else {
pending_uninstall_confirmation = Some(pending);
overlay.show_and_focus();
overlay
.set_status_text(&format!("Launch error: {error}"));
}
}
}
return;
}
if selected_id == ACTION_UNINSTALL_CANCEL_ID {
let Some(pending) = pending_uninstall_confirmation.take() else {
return;
};
current_results = pending.previous_results;
selected_index = pending
.previous_selected_index
.min(current_results.len().saturating_sub(1));
if current_results.is_empty() {
set_status_row_overlay_state(
&overlay,
if pending.previous_command_mode {
STATUS_ROW_NO_COMMAND_RESULTS
} else {
STATUS_ROW_NO_RESULTS
},
);
} else {
let rows = overlay_rows(
¤t_results,
pending.previous_command_mode,
);
overlay.set_results(&rows, selected_index);
}
overlay.set_status_text("");
return;
}
pending_uninstall_confirmation = None;
}
let selected_is_uninstall = selected
.id
.starts_with(crate::uninstall_registry::ACTION_UNINSTALL_PREFIX);
if selected_is_uninstall {
let parsed_query = ParsedQuery::parse(
overlay.query_text().trim(),
runtime_config.search_dsl_enabled,
);
pending_uninstall_confirmation = Some(PendingUninstallConfirmation {
uninstall_action: selected.clone(),
previous_results: current_results.clone(),
previous_selected_index: selected_index,
previous_command_mode: parsed_query.command_mode,
});
current_results = uninstall_confirmation_results(selected);
selected_index = 0;
let rows = overlay_rows(¤t_results, true);
overlay.set_results(&rows, selected_index);
overlay.set_status_text("");
return;
}
if selected.id == ACTION_TRIM_MEMORY_ID {
search_session.clear();
overlay.trim_runtime_memory();
overlay.set_status_text("Memory caches trimmed");
return;
}
match launch_overlay_selection(
&service,
&runtime_config,
&plugin_registry,
¤t_results,
selected_index,
last_query.as_str(),
) {
Ok(()) => {
overlay.set_status_text("");
overlay.hide_now();
overlay_state.on_escape();
reset_overlay_session(
&overlay,
&mut current_results,
&mut selected_index,
);
pending_uninstall_confirmation = None;
last_query.clear();
search_session.clear();
}
Err(error) => {
overlay.set_status_text(&format!("Launch error: {error}"));
}
}
}
}
})
.map_err(RuntimeError::Overlay)?;
registrar.unregister_all()?;
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
log_info("[nex] non-windows runtime mode: no global hotkey loop");
Ok(())
}
}
fn command_ensure_config() -> Result<(), RuntimeError> {
let cfg = config::load(None)?;
if !cfg.config_path.exists() {
config::write_user_template(&cfg, &cfg.config_path)?;
log_info(&format!(
"[nex] wrote user config template to {}",
cfg.config_path.display()
));
}
log_info(&format!(
"[nex] config ready at {}",
cfg.config_path.display()
));
Ok(())
}
fn command_sync_startup() -> Result<(), RuntimeError> {
#[cfg(target_os = "windows")]
{
let cfg = config::load(None)?;
let exe = std::env::current_exe()?;
crate::startup::set_enabled(cfg.launch_at_startup, &exe)?;
log_info(&format!(
"[nex] startup registration synced: enabled={}",
cfg.launch_at_startup
));
return Ok(());
}
#[cfg(not(target_os = "windows"))]
{
log_info("[nex] startup sync is unsupported on this platform");
Ok(())
}
}
fn command_set_launch_at_startup(enabled: bool) -> Result<(), RuntimeError> {
let mut cfg = config::load(None)?;
cfg.launch_at_startup = enabled;
config::save(&cfg)?;
#[cfg(target_os = "windows")]
{
let exe = std::env::current_exe()?;
crate::startup::set_enabled(enabled, &exe)?;
}
log_info(&format!(
"[nex] launch_at_startup updated: enabled={} (can be changed in config)",
enabled
));
Ok(())
}
fn command_status() -> Result<(), RuntimeError> {
#[cfg(target_os = "windows")]
{
let state = inspect_runtime_process_state();
let running = state.has_overlay_window;
log_info(&format!(
"[nex] status: {}",
if running {
"running"
} else if !state.other_runtime_pids.is_empty() {
"degraded (process without overlay window)"
} else {
"stopped"
}
));
if !state.other_runtime_pids.is_empty() {
log_warn(&format!(
"[nex] status detected runtime_pids_without_window={:?} recommendation=run --restart",
state.other_runtime_pids
));
}
if let Some(snapshot) = load_status_diagnostics_snapshot() {
if let Some(line) = snapshot.hotkey_registration_issue_line {
log_warn(&format!("[nex] status last_hotkey_issue {line}"));
}
if let Some(line) = snapshot.overlay_ready_line {
log_info(&format!("[nex] status last_overlay_ready {line}"));
}
if let Some(line) = snapshot.hotkey_ready_line {
log_info(&format!("[nex] status last_hotkey_ready {line}"));
}
if let Some(line) = snapshot.indexing_started_line {
log_info(&format!("[nex] status last_indexing_started {line}"));
}
if let Some(line) = snapshot.indexing_completed_line {
log_info(&format!("[nex] status last_indexing_completed {line}"));
}
if let Some(line) = snapshot.cache_applied_line {
log_info(&format!("[nex] status last_cache_applied {line}"));
}
if let Some(line) = snapshot.startup_index_line {
log_info(&format!("[nex] status last_indexing {line}"));
}
if let Some(line) = snapshot.last_provider_line {
log_info(&format!("[nex] status last_provider {line}"));
}
if let Some(line) = snapshot.last_provider_freshness_line {
log_info(&format!("[nex] status last_provider_freshness {line}"));
}
if let Some(line) = snapshot.last_stale_prune_line {
log_info(&format!("[nex] status last_stale_prune {line}"));
}
if let Some(line) = snapshot.last_cache_compaction_line {
log_info(&format!("[nex] status last_cache_compaction {line}"));
}
if let Some(line) = snapshot.last_icon_cache_line {
log_info(&format!("[nex] status last_icon_cache {line}"));
}
if let Some(line) = snapshot.last_overlay_tuning_line {
log_info(&format!(
"[nex] status last_overlay_tuning {line}"
));
}
if let Some(line) = snapshot.last_memory_snapshot_line {
log_info(&format!(
"[nex] status last_memory_snapshot {line}"
));
}
if let Some(line) = snapshot.last_config_reload_line {
log_info(&format!(
"[nex] status last_config_reload {line}"
));
}
}
if let Some(report) = load_query_profile_status_report() {
if let Some(recent) = report.recent {
log_info(&format!(
"[nex] status query_latency_recent samples={} p50_ms={} p95_ms={} p99_ms={} max_ms={} avg_ms={} indexed_p95_ms={} short_q_samples={} short_q_p95_ms={} short_q_app_bias_rate={}%",
recent.samples,
recent.p50_total_ms,
recent.p95_total_ms,
recent.p99_total_ms,
recent.max_total_ms,
recent.avg_total_ms,
recent.p95_indexed_ms,
recent.short_query_samples,
recent.short_query_p95_total_ms,
recent.short_query_app_bias_rate_pct
));
}
if let Some(historical) = report.historical {
log_info(&format!(
"[nex] status query_latency_historical samples={} p50_ms={} p95_ms={} p99_ms={} max_ms={} avg_ms={} indexed_p95_ms={} short_q_samples={} short_q_p95_ms={} short_q_app_bias_rate={}%",
historical.samples,
historical.p50_total_ms,
historical.p95_total_ms,
historical.p99_total_ms,
historical.max_total_ms,
historical.avg_total_ms,
historical.p95_indexed_ms,
historical.short_query_samples,
historical.short_query_p95_total_ms,
historical.short_query_app_bias_rate_pct
));
}
log_info(&format!(
"[nex] status query_guard recent_skipped_symbol_queries={} historical_skipped_symbol_queries={}",
report.recent_skipped_symbol_queries, report.historical_skipped_symbol_queries
));
}
return Ok(());
}
#[cfg(not(target_os = "windows"))]
{
log_info("[nex] status: unsupported on this platform");
Ok(())
}
}
fn command_status_json() -> Result<(), RuntimeError> {
#[cfg(target_os = "windows")]
{
let state = inspect_runtime_process_state();
let lifecycle = if state.has_overlay_window {
"running"
} else if !state.other_runtime_pids.is_empty() {
"degraded"
} else {
"stopped"
};
let snapshot = load_status_diagnostics_snapshot();
let report = load_query_profile_status_report();
let diagnostics = snapshot
.as_ref()
.map(build_status_diagnostics_json)
.unwrap_or_else(|| serde_json::json!({}));
let payload = serde_json::json!({
"runtime_state": lifecycle,
"has_overlay_window": state.has_overlay_window,
"other_runtime_pids": state.other_runtime_pids,
"diagnostics": diagnostics,
"query_latency": report.map(query_profile_report_json),
});
let encoded = serde_json::to_string_pretty(&payload)
.map_err(|error| RuntimeError::Args(format!("status-json encode error: {error}")))?;
println!("{encoded}");
return Ok(());
}
#[cfg(not(target_os = "windows"))]
{
let payload = serde_json::json!({
"runtime_state": "unsupported_platform",
"has_overlay_window": false,
"other_runtime_pids": Vec::<u32>::new(),
"diagnostics": serde_json::json!({}),
"query_latency": serde_json::Value::Null,
});
let encoded = serde_json::to_string_pretty(&payload)
.map_err(|error| RuntimeError::Args(format!("status-json encode error: {error}")))?;
println!("{encoded}");
Ok(())
}
}
fn command_diagnostics_bundle() -> Result<(), RuntimeError> {
let cfg = config::load(None)?;
let output_dir = write_diagnostics_bundle(&cfg)?;
log_info(&format!(
"[nex] diagnostics bundle written to {}",
output_dir.display()
));
Ok(())
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct StatusDiagnosticsSnapshot {
hotkey_registration_issue_line: Option<String>,
overlay_ready_line: Option<String>,
hotkey_ready_line: Option<String>,
indexing_started_line: Option<String>,
indexing_completed_line: Option<String>,
cache_applied_line: Option<String>,
startup_index_line: Option<String>,
last_provider_line: Option<String>,
last_provider_freshness_line: Option<String>,
last_stale_prune_line: Option<String>,
last_cache_compaction_line: Option<String>,
last_icon_cache_line: Option<String>,
last_overlay_tuning_line: Option<String>,
last_memory_snapshot_line: Option<String>,
last_config_reload_line: Option<String>,
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct QueryProfileSample {
total_ms: u128,
indexed_ms: u128,
query_len: usize,
short_app_bias: bool,
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct QueryProfileSummary {
samples: usize,
p50_total_ms: u128,
p95_total_ms: u128,
p99_total_ms: u128,
max_total_ms: u128,
avg_total_ms: u128,
p95_indexed_ms: u128,
short_query_samples: usize,
short_query_p95_total_ms: u128,
short_query_app_bias_rate_pct: u8,
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[derive(Debug, Clone, PartialEq, Eq)]
struct QueryProfileStatusReport {
recent: Option<QueryProfileSummary>,
historical: Option<QueryProfileSummary>,
recent_skipped_symbol_queries: usize,
historical_skipped_symbol_queries: usize,
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn load_status_diagnostics_snapshot() -> Option<StatusDiagnosticsSnapshot> {
let content = crate::logging::candidate_log_paths()
.into_iter()
.find_map(|log_path| std::fs::read_to_string(log_path).ok())?;
parse_status_diagnostics_snapshot(&content)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn load_query_profile_status_report() -> Option<QueryProfileStatusReport> {
let content = crate::logging::candidate_log_paths()
.into_iter()
.find_map(|log_path| std::fs::read_to_string(log_path).ok())?;
summarize_query_profile_status_report(&content)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_status_diagnostics_snapshot(content: &str) -> Option<StatusDiagnosticsSnapshot> {
let hotkey_registration_issue_line =
latest_line_with_token(content, "hotkey_registration_issue ");
let overlay_ready_line = latest_line_with_token(content, "startup_phase phase=overlay_ready ");
let hotkey_ready_line = latest_line_with_token(content, "startup_phase phase=hotkey_ready ");
let indexing_started_line =
latest_line_with_token(content, "startup_phase phase=indexing_started ");
let indexing_completed_line =
latest_line_with_token(content, "startup_phase phase=indexing_completed ");
let cache_applied_line = latest_line_with_token(content, "startup_phase phase=cache_applied ");
let startup_index_line = latest_line_with_token(content, "startup indexed_items=");
let last_provider_line = latest_line_with_token(content, "index_provider name=");
let last_provider_freshness_line = latest_line_with_token(content, "provider_freshness ");
let last_stale_prune_line = latest_line_with_token(content, "stale_prune ");
let last_cache_compaction_line = latest_line_with_token(content, "cache_compaction ");
let last_icon_cache_line = latest_line_with_token(content, "overlay_icon_cache reason=");
let last_overlay_tuning_line = latest_line_with_token(content, "overlay_tuning ");
let last_memory_snapshot_line = latest_line_with_token(content, "memory_snapshot reason=");
let last_config_reload_line = latest_line_with_token(content, "config reloaded ");
if hotkey_registration_issue_line.is_none()
&& overlay_ready_line.is_none()
&& hotkey_ready_line.is_none()
&& indexing_started_line.is_none()
&& indexing_completed_line.is_none()
&& cache_applied_line.is_none()
&& startup_index_line.is_none()
&& last_provider_line.is_none()
&& last_provider_freshness_line.is_none()
&& last_stale_prune_line.is_none()
&& last_cache_compaction_line.is_none()
&& last_icon_cache_line.is_none()
&& last_overlay_tuning_line.is_none()
&& last_memory_snapshot_line.is_none()
&& last_config_reload_line.is_none()
{
return None;
}
Some(StatusDiagnosticsSnapshot {
hotkey_registration_issue_line,
overlay_ready_line,
hotkey_ready_line,
indexing_started_line,
indexing_completed_line,
cache_applied_line,
startup_index_line,
last_provider_line,
last_provider_freshness_line,
last_stale_prune_line,
last_cache_compaction_line,
last_icon_cache_line,
last_overlay_tuning_line,
last_memory_snapshot_line,
last_config_reload_line,
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn latest_line_with_token(content: &str, token: &str) -> Option<String> {
content
.lines()
.rev()
.find(|line| line.contains(token))
.map(str::to_string)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn summarize_query_profile_status_report(content: &str) -> Option<QueryProfileStatusReport> {
let recent_samples = parse_recent_query_profile_samples(content);
let historical_samples = parse_query_profile_samples(content);
let recent = summarize_query_profile_samples(&recent_samples);
let historical = summarize_query_profile_samples(&historical_samples);
if recent.is_none() && historical.is_none() {
return None;
}
let recent_lines = recent_runtime_log_slice(content);
let recent_skipped_symbol_queries = count_skipped_symbol_query_guards(recent_lines);
let historical_skipped_symbol_queries = count_skipped_symbol_query_guards(content);
Some(QueryProfileStatusReport {
recent,
historical,
recent_skipped_symbol_queries,
historical_skipped_symbol_queries,
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn build_status_diagnostics_json(snapshot: &StatusDiagnosticsSnapshot) -> serde_json::Value {
let hotkey_issue = build_phase_status_json(snapshot.hotkey_registration_issue_line.as_ref());
let overlay_ready = build_phase_status_json(snapshot.overlay_ready_line.as_ref());
let hotkey_ready = build_phase_status_json(snapshot.hotkey_ready_line.as_ref());
let indexing_started = build_phase_status_json(snapshot.indexing_started_line.as_ref());
let indexing_completed = build_phase_status_json(snapshot.indexing_completed_line.as_ref());
let cache_applied = build_phase_status_json(snapshot.cache_applied_line.as_ref());
let startup_indexing = snapshot
.startup_index_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let provider = snapshot
.last_provider_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let provider_freshness = snapshot
.last_provider_freshness_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let stale_prune = snapshot
.last_stale_prune_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let cache_compaction = snapshot
.last_cache_compaction_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let icon_cache = snapshot
.last_icon_cache_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let overlay_tuning = snapshot
.last_overlay_tuning_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let memory_snapshot = snapshot
.last_memory_snapshot_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let config_reload = snapshot
.last_config_reload_line
.as_ref()
.and_then(|line| parse_key_value_tokens(line));
let config_reload_epoch_secs = snapshot
.last_config_reload_line
.as_ref()
.and_then(|line| parse_log_line_epoch_secs(line));
serde_json::json!({
"startup_lifecycle": {
"overlay_ready": overlay_ready,
"hotkey_ready": hotkey_ready,
"indexing_started": indexing_started,
"indexing_completed": indexing_completed,
"cache_applied": cache_applied,
},
"hotkey_issue": hotkey_issue,
"startup_indexing": startup_indexing,
"provider": provider,
"provider_freshness": provider_freshness,
"stale_prune": stale_prune,
"cache_compaction": cache_compaction,
"icon_cache": icon_cache,
"overlay_tuning": overlay_tuning,
"memory_snapshot": memory_snapshot,
"config_reload": config_reload,
"config_reload_epoch_secs": config_reload_epoch_secs,
"raw": {
"hotkey_issue_line": snapshot.hotkey_registration_issue_line,
"overlay_ready_line": snapshot.overlay_ready_line,
"hotkey_ready_line": snapshot.hotkey_ready_line,
"indexing_started_line": snapshot.indexing_started_line,
"indexing_completed_line": snapshot.indexing_completed_line,
"cache_applied_line": snapshot.cache_applied_line,
"startup_indexing_line": snapshot.startup_index_line,
"provider_line": snapshot.last_provider_line,
"provider_freshness_line": snapshot.last_provider_freshness_line,
"stale_prune_line": snapshot.last_stale_prune_line,
"cache_compaction_line": snapshot.last_cache_compaction_line,
"icon_cache_line": snapshot.last_icon_cache_line,
"overlay_tuning_line": snapshot.last_overlay_tuning_line,
"memory_snapshot_line": snapshot.last_memory_snapshot_line,
"config_reload_line": snapshot.last_config_reload_line,
}
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn build_phase_status_json(line: Option<&String>) -> serde_json::Value {
let tokens = line.and_then(|value| parse_key_value_tokens(value));
let epoch_secs = line.and_then(|value| parse_log_line_epoch_secs(value));
serde_json::json!({
"tokens": tokens,
"epoch_secs": epoch_secs,
"line": line.cloned(),
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn query_profile_report_json(report: QueryProfileStatusReport) -> serde_json::Value {
serde_json::json!({
"recent": report.recent.map(query_profile_summary_json),
"historical": report.historical.map(query_profile_summary_json),
"recent_skipped_symbol_queries": report.recent_skipped_symbol_queries,
"historical_skipped_symbol_queries": report.historical_skipped_symbol_queries,
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn query_profile_summary_json(summary: QueryProfileSummary) -> serde_json::Value {
serde_json::json!({
"samples": summary.samples,
"p50_total_ms": summary.p50_total_ms,
"p95_total_ms": summary.p95_total_ms,
"p99_total_ms": summary.p99_total_ms,
"max_total_ms": summary.max_total_ms,
"avg_total_ms": summary.avg_total_ms,
"p95_indexed_ms": summary.p95_indexed_ms,
"short_query_samples": summary.short_query_samples,
"short_query_p95_total_ms": summary.short_query_p95_total_ms,
"short_query_app_bias_rate_pct": summary.short_query_app_bias_rate_pct,
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_log_line_epoch_secs(line: &str) -> Option<u64> {
let trimmed = line.trim();
let start = trimmed.find('[')? + 1;
let end = trimmed[start..].find(']')? + start;
trimmed[start..end].parse::<u64>().ok()
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_key_value_tokens(line: &str) -> Option<serde_json::Value> {
let mut map = serde_json::Map::new();
for token in line.split_whitespace() {
let Some((key, value)) = token.split_once('=') else {
continue;
};
let key = key.trim().trim_end_matches(':');
if key.is_empty() {
continue;
}
let value = value.trim().trim_end_matches(',');
if value.is_empty() {
continue;
}
if let Ok(number) = value.parse::<u64>() {
map.insert(key.to_string(), serde_json::json!(number));
continue;
}
if let Ok(number) = value.parse::<f64>() {
map.insert(key.to_string(), serde_json::json!(number));
continue;
}
if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false") {
map.insert(
key.to_string(),
serde_json::json!(value.eq_ignore_ascii_case("true")),
);
continue;
}
map.insert(
key.to_string(),
serde_json::json!(value.trim_matches('"').to_string()),
);
}
if map.is_empty() {
None
} else {
Some(serde_json::Value::Object(map))
}
}
#[cfg(test)]
fn summarize_query_profiles(content: &str) -> Option<QueryProfileSummary> {
let samples = parse_recent_query_profile_samples(content);
summarize_query_profile_samples(&samples)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn summarize_query_profile_samples(samples: &[QueryProfileSample]) -> Option<QueryProfileSummary> {
let mut samples = samples.to_vec();
if samples.is_empty() {
return None;
}
if samples.len() > QUERY_PROFILE_STATUS_SAMPLE_WINDOW {
samples.drain(0..(samples.len() - QUERY_PROFILE_STATUS_SAMPLE_WINDOW));
}
if samples.is_empty() {
return None;
}
let mut total_ms: Vec<u128> = samples.iter().map(|sample| sample.total_ms).collect();
let mut indexed_ms: Vec<u128> = samples.iter().map(|sample| sample.indexed_ms).collect();
let max_total_ms = total_ms.iter().copied().max().unwrap_or(0);
let avg_total_ms = total_ms.iter().sum::<u128>() / (total_ms.len() as u128);
let p50_total_ms = percentile_u128(&mut total_ms, 0.50);
let p95_total_ms = percentile_u128(&mut total_ms, 0.95);
let p99_total_ms = percentile_u128(&mut total_ms, 0.99);
let p95_indexed_ms = percentile_u128(&mut indexed_ms, 0.95);
let short_query_samples: Vec<QueryProfileSample> = samples
.iter()
.copied()
.filter(|sample| sample.query_len <= SHORT_QUERY_APP_BIAS_MAX_LEN)
.collect();
let short_query_samples_count = short_query_samples.len();
let mut short_total_ms: Vec<u128> = short_query_samples
.iter()
.map(|sample| sample.total_ms)
.collect();
let short_query_p95_total_ms = percentile_u128(&mut short_total_ms, 0.95);
let short_query_app_bias_count = short_query_samples
.iter()
.filter(|sample| sample.short_app_bias)
.count();
let short_query_app_bias_rate_pct = if short_query_samples_count == 0 {
0
} else {
((short_query_app_bias_count * 100) / short_query_samples_count) as u8
};
Some(QueryProfileSummary {
samples: samples.len(),
p50_total_ms,
p95_total_ms,
p99_total_ms,
max_total_ms,
avg_total_ms,
p95_indexed_ms,
short_query_samples: short_query_samples_count,
short_query_p95_total_ms,
short_query_app_bias_rate_pct,
})
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn recent_runtime_log_slice(content: &str) -> &str {
let Some(pos) = rfind_runtime_log_marker(content, "startup mode=") else {
return content;
};
let line_start = content[..pos].rfind('\n').map(|idx| idx + 1).unwrap_or(pos);
&content[line_start..]
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn count_skipped_symbol_query_guards(content: &str) -> usize {
content
.lines()
.filter(|line| line.contains("query_guard skip=non_searchable_symbol_only"))
.count()
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_recent_query_profile_samples(content: &str) -> Vec<QueryProfileSample> {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return Vec::new();
}
let start_index = lines
.iter()
.rposition(|line| line_contains_runtime_log_marker(line, "startup mode="))
.unwrap_or(0);
parse_query_profile_samples(&lines[start_index..].join("\n"))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_query_profile_samples(content: &str) -> Vec<QueryProfileSample> {
content
.lines()
.filter(|line| line_contains_runtime_log_marker(line, "query_profile "))
.filter_map(|line| {
let total_ms = parse_u128_field(line, "total_ms=")?;
let indexed_ms = parse_u128_field(line, "indexed_ms=").unwrap_or(0);
let query = parse_quoted_field(line, "q=").unwrap_or_default();
let query_len = query.chars().count();
let short_app_bias = parse_bool_field(line, "short_app_bias=").unwrap_or(false);
Some(QueryProfileSample {
total_ms,
indexed_ms,
query_len,
short_app_bias,
})
})
.collect()
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_u128_field(line: &str, key: &str) -> Option<u128> {
let start = line.find(key)? + key.len();
let tail = &line[start..];
let value = tail
.split_whitespace()
.next()
.map(|part| part.trim_end_matches(','))
.unwrap_or_default();
value.parse::<u128>().ok()
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_bool_field(line: &str, key: &str) -> Option<bool> {
let start = line.find(key)? + key.len();
let tail = &line[start..];
let value = tail
.split_whitespace()
.next()
.map(|part| part.trim_end_matches(','))
.unwrap_or_default();
match value {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_quoted_field(line: &str, key: &str) -> Option<String> {
let start = line.find(key)? + key.len();
let tail = &line[start..];
if !tail.starts_with('"') {
return None;
}
let end = tail[1..].find('"')?;
Some(tail[1..(1 + end)].to_string())
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn percentile_u128(values: &mut [u128], percentile: f64) -> u128 {
if values.is_empty() {
return 0;
}
values.sort_unstable();
let last = values.len().saturating_sub(1);
let idx = ((last as f64) * percentile.clamp(0.0, 1.0)).round() as usize;
values[idx.min(last)]
}
fn command_quit() -> Result<(), RuntimeError> {
#[cfg(target_os = "windows")]
{
match stop_runtime_instance(std::time::Duration::from_secs(3))? {
StopRuntimeOutcome::AlreadyStopped => {
log_info("[nex] quit skipped (not running)");
Ok(())
}
StopRuntimeOutcome::Graceful => {
log_info("[nex] quit completed (graceful)");
Ok(())
}
StopRuntimeOutcome::Forced => {
log_warn("[nex] quit required forced process termination");
Ok(())
}
StopRuntimeOutcome::Failed => Err(RuntimeError::Overlay(
"quit failed: runtime is still active after graceful and forced attempts"
.to_string(),
)),
}
}
#[cfg(not(target_os = "windows"))]
{
log_info("[nex] quit is unsupported on this platform");
Ok(())
}
}
fn command_restart() -> Result<(), RuntimeError> {
#[cfg(target_os = "windows")]
{
match stop_runtime_instance(std::time::Duration::from_secs(3))? {
StopRuntimeOutcome::Failed => {
return Err(RuntimeError::Overlay(
"restart failed: existing runtime could not be stopped".to_string(),
));
}
StopRuntimeOutcome::Forced => {
log_warn("[nex] restart required forced process termination");
}
StopRuntimeOutcome::Graceful | StopRuntimeOutcome::AlreadyStopped => {}
}
run_with_options(RuntimeOptions::default())
}
#[cfg(not(target_os = "windows"))]
{
run_with_options(RuntimeOptions::default())
}
}
#[cfg(target_os = "windows")]
#[derive(Debug, Clone, PartialEq, Eq)]
struct RuntimeProcessState {
has_overlay_window: bool,
other_runtime_pids: Vec<u32>,
}
#[cfg(target_os = "windows")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StopRuntimeOutcome {
AlreadyStopped,
Graceful,
Forced,
Failed,
}
#[cfg(target_os = "windows")]
fn inspect_runtime_process_state() -> RuntimeProcessState {
RuntimeProcessState {
has_overlay_window: is_instance_window_present(),
other_runtime_pids: runtime_process_pids_excluding_current().unwrap_or_default(),
}
}
#[cfg(target_os = "windows")]
fn stop_runtime_instance(timeout: std::time::Duration) -> Result<StopRuntimeOutcome, RuntimeError> {
let mut state = inspect_runtime_process_state();
if !state.has_overlay_window && state.other_runtime_pids.is_empty() {
return Ok(StopRuntimeOutcome::AlreadyStopped);
}
if state.has_overlay_window {
let _ = signal_existing_instance_quit().map_err(RuntimeError::Overlay)?;
if wait_until_overlay_window_closed(timeout) {
state = inspect_runtime_process_state();
if state.other_runtime_pids.is_empty() {
return Ok(StopRuntimeOutcome::Graceful);
}
}
}
let forced = force_terminate_other_runtime_processes()?;
std::thread::sleep(std::time::Duration::from_millis(250));
let post = inspect_runtime_process_state();
if !post.has_overlay_window && post.other_runtime_pids.is_empty() {
if forced {
Ok(StopRuntimeOutcome::Forced)
} else {
Ok(StopRuntimeOutcome::Graceful)
}
} else {
Ok(StopRuntimeOutcome::Failed)
}
}
#[cfg(target_os = "windows")]
fn wait_until_overlay_window_closed(timeout: std::time::Duration) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if !is_instance_window_present() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(120));
}
!is_instance_window_present()
}
#[cfg(target_os = "windows")]
fn force_terminate_other_runtime_processes() -> Result<bool, RuntimeError> {
let current_pid = unsafe { windows_sys::Win32::System::Threading::GetCurrentProcessId() };
let mut terminated_any = false;
for exe_name in runtime_executable_names() {
let command = format!(
"taskkill /F /T /FI \"IMAGENAME eq {exe_name}\" /FI \"PID ne {}\" >NUL 2>&1",
current_pid
);
let status = std::process::Command::new("cmd")
.arg("/C")
.arg(command)
.status()
.map_err(RuntimeError::Io)?;
terminated_any |= status.success();
}
Ok(terminated_any)
}
#[cfg(target_os = "windows")]
fn runtime_process_pids_excluding_current() -> Result<Vec<u32>, RuntimeError> {
let current_pid = unsafe { windows_sys::Win32::System::Threading::GetCurrentProcessId() };
let mut pids = Vec::new();
for exe_name in runtime_executable_names() {
let output = std::process::Command::new("cmd")
.arg("/C")
.arg(format!("tasklist /FI \"IMAGENAME eq {exe_name}\" /FO LIST /NH"))
.output()
.map_err(RuntimeError::Io)?;
let stdout = String::from_utf8_lossy(&output.stdout);
pids.extend(parse_tasklist_pid_lines(&stdout));
}
pids.retain(|pid| *pid != current_pid);
pids.sort_unstable();
pids.dedup();
Ok(pids)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn parse_tasklist_pid_lines(content: &str) -> Vec<u32> {
content
.lines()
.filter_map(|line| {
let trimmed = line.trim();
if !trimmed.to_ascii_lowercase().starts_with("pid:") {
return None;
}
let value = trimmed.split(':').nth(1)?.trim();
value.parse::<u32>().ok()
})
.collect()
}
fn write_diagnostics_bundle(cfg: &config::Config) -> Result<std::path::PathBuf, RuntimeError> {
let support_dir = config::stable_app_data_dir().join("support");
std::fs::create_dir_all(&support_dir)?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let bundle_dir = support_dir.join(format!("diagnostics-{stamp}"));
std::fs::create_dir_all(&bundle_dir)?;
let running_state = runtime_state_summary();
let summary = format!(
"nex diagnostics bundle\ngenerated_epoch_secs={stamp}\nruntime_state={running_state}\nconfig_path={}\nindex_db_path={}\nlogs_dir={}\n",
cfg.config_path.display(),
cfg.index_db_path.display(),
crate::logging::logs_dir().display()
);
std::fs::write(bundle_dir.join("summary.txt"), summary)?;
if cfg.config_path.exists() {
let raw_ext = cfg
.config_path
.extension()
.and_then(|ext| ext.to_str())
.filter(|ext| !ext.trim().is_empty())
.unwrap_or("txt");
let _ = std::fs::copy(
&cfg.config_path,
bundle_dir.join(format!("config.raw.{raw_ext}")),
);
}
let sanitized_cfg = serde_json::json!({
"version": cfg.version,
"max_results": cfg.max_results,
"hotkey": cfg.hotkey,
"launch_at_startup": cfg.launch_at_startup,
"search_mode_default": cfg.search_mode_default,
"search_dsl_enabled": cfg.search_dsl_enabled,
"search_query_results_with_delay": cfg.search_query_results_with_delay,
"search_delay_time_ms": cfg.search_delay_time_ms,
"uninstall_actions_enabled": cfg.uninstall_actions_enabled,
"web_search_provider": cfg.web_search_provider,
"clipboard_enabled": cfg.clipboard_enabled,
"clipboard_retention_minutes": cfg.clipboard_retention_minutes,
"clipboard_exclude_sensitive_patterns_count": cfg.clipboard_exclude_sensitive_patterns.len(),
"plugins_enabled": cfg.plugins_enabled,
"plugin_paths_count": cfg.plugin_paths.len(),
"plugins_safe_mode": cfg.plugins_safe_mode,
"game_mode_enabled": cfg.game_mode_enabled,
"idle_cache_trim_ms": cfg.idle_cache_trim_ms,
"active_memory_target_mb": cfg.active_memory_target_mb,
"index_max_items_total": cfg.index_max_items_total,
"index_max_items_per_root": cfg.index_max_items_per_root,
"index_max_items_per_query_seed": cfg.index_max_items_per_query_seed,
"discovery_roots_count": cfg.discovery_roots.len(),
"discovery_exclude_roots_count": cfg.discovery_exclude_roots.len(),
"windows_search_enabled": cfg.windows_search_enabled,
"windows_search_fallback_filesystem": cfg.windows_search_fallback_filesystem,
"show_files": cfg.show_files,
"show_folders": cfg.show_folders
});
let encoded = serde_json::to_string_pretty(&sanitized_cfg)
.map_err(|e| RuntimeError::Args(format!("failed to encode sanitized config: {e}")))?;
std::fs::write(bundle_dir.join("config.sanitized.json"), encoded)?;
copy_recent_logs_to_bundle(&crate::logging::logs_dir(), &bundle_dir.join("logs"))?;
Ok(bundle_dir)
}
fn copy_recent_logs_to_bundle(
source_logs_dir: &std::path::Path,
target_logs_dir: &std::path::Path,
) -> Result<(), RuntimeError> {
std::fs::create_dir_all(target_logs_dir)?;
if !source_logs_dir.exists() {
return Ok(());
}
let mut entries = std::fs::read_dir(source_logs_dir)?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".log"))
.unwrap_or(false)
})
.collect::<Vec<_>>();
entries.sort_by_key(|path| {
std::fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
entries.reverse();
for path in entries.into_iter().take(5) {
if let Some(name) = path.file_name() {
let _ = std::fs::copy(&path, target_logs_dir.join(name));
}
}
Ok(())
}
fn runtime_state_summary() -> String {
#[cfg(target_os = "windows")]
{
let state = inspect_runtime_process_state();
if state.has_overlay_window {
return "running".to_string();
}
if !state.other_runtime_pids.is_empty() {
return format!(
"degraded(process_without_overlay_window pids={:?})",
state.other_runtime_pids
);
}
"stopped".to_string()
}
#[cfg(not(target_os = "windows"))]
{
"unsupported_platform".to_string()
}
}
#[cfg(target_os = "windows")]
fn spawn_background_process() -> Result<(), RuntimeError> {
use std::os::windows::process::CommandExt;
let exe = std::env::current_exe()?;
let mut command = std::process::Command::new(exe);
command.arg("--foreground");
command.env("NEX_SUPPRESS_STDIO", "1");
command.creation_flags(0x00000008 | 0x00000200 | 0x08000000);
command.stdin(std::process::Stdio::null());
command.stdout(std::process::Stdio::null());
command.stderr(std::process::Stdio::null());
command.spawn()?;
log_info("[nex] background process started");
Ok(())
}
fn runtime_mode() -> &'static str {
#[cfg(target_os = "windows")]
{
"windows-hotkey-runtime"
}
#[cfg(not(target_os = "windows"))]
{
"non-windows-noop"
}
}
#[cfg(target_os = "windows")]
fn overlay_rows(results: &[crate::model::SearchItem], command_mode: bool) -> Vec<OverlayRow> {
if results.is_empty() {
return Vec::new();
}
if command_mode {
return results
.iter()
.enumerate()
.map(|(index, item)| result_row(item, index, OverlayRowRole::Item, command_mode))
.collect();
}
let mut rows = Vec::new();
rows.push(result_row(
&results[0],
0,
OverlayRowRole::TopHit,
command_mode,
));
let mut app_indices = Vec::new();
let mut file_indices = Vec::new();
let mut action_indices = Vec::new();
let mut clipboard_indices = Vec::new();
let mut other_indices = Vec::new();
for (index, item) in results.iter().enumerate().skip(1) {
if item.kind.eq_ignore_ascii_case("app") {
app_indices.push(index);
} else if item.kind.eq_ignore_ascii_case("file") || item.kind.eq_ignore_ascii_case("folder")
{
file_indices.push(index);
} else if item.kind.eq_ignore_ascii_case("action") {
action_indices.push(index);
} else if item.kind.eq_ignore_ascii_case("clipboard") {
clipboard_indices.push(index);
} else {
other_indices.push(index);
}
}
append_group_rows(&mut rows, &app_indices, results, command_mode);
append_group_rows(&mut rows, &file_indices, results, command_mode);
append_group_rows(&mut rows, &action_indices, results, command_mode);
append_group_rows(&mut rows, &clipboard_indices, results, command_mode);
append_group_rows(&mut rows, &other_indices, results, command_mode);
rows
}
#[cfg(target_os = "windows")]
fn append_group_rows(
rows: &mut Vec<OverlayRow>,
indices: &[usize],
results: &[crate::model::SearchItem],
command_mode: bool,
) {
if indices.is_empty() {
return;
}
for index in indices {
rows.push(result_row(
&results[*index],
*index,
OverlayRowRole::Item,
command_mode,
));
}
}
#[cfg(target_os = "windows")]
fn result_row(
item: &crate::model::SearchItem,
result_index: usize,
role: OverlayRowRole,
command_mode: bool,
) -> OverlayRow {
OverlayRow {
role,
result_index: result_index as i32,
kind: item.kind.clone(),
title: item.title.clone(),
path: overlay_subtitle(item, command_mode),
icon_path: item.path.clone(),
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn dedupe_overlay_results(results: &mut Vec<crate::model::SearchItem>) {
let app_title_keys: std::collections::HashSet<String> = results
.iter()
.filter(|item| item.kind.eq_ignore_ascii_case("app"))
.filter(|item| !should_hide_known_start_menu_doc_sample_entry(item))
.filter_map(|item| {
let key = normalize_title_key(&item.title);
if key.is_empty() {
None
} else {
Some(key)
}
})
.collect();
let mut seen_app_titles = std::collections::HashSet::new();
let mut seen_other_paths = std::collections::HashSet::new();
results.retain(|item| {
if item.kind.eq_ignore_ascii_case("app") {
if should_hide_known_start_menu_doc_sample_entry(item) {
return false;
}
let key = normalize_title_key(&item.title);
if key.is_empty() {
return true;
}
return seen_app_titles.insert(key);
}
if item.kind.eq_ignore_ascii_case("file")
&& is_windows_shortcut_path(&item.path)
&& app_title_keys.contains(&shortcut_base_title_key(&item.title))
{
return false;
}
let key = normalize_path_key(&item.path);
if key.is_empty() {
return true;
}
seen_other_paths.insert(key)
});
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn should_hide_known_start_menu_doc_sample_entry(item: &crate::model::SearchItem) -> bool {
if !item.kind.eq_ignore_ascii_case("app") {
return false;
}
let lower = item.title.trim().to_ascii_lowercase();
let path_lower = item.path.trim().replace('/', "\\").to_ascii_lowercase();
let is_shell_appsfolder = path_lower.starts_with("shell:appsfolder\\");
if path_lower.contains("\\windows kits\\10\\shortcuts\\") && path_lower.ends_with(".url") {
return true;
}
if has_non_app_document_extension(path_lower.as_str()) {
return true;
}
if is_shell_appsfolder && path_lower.contains("://") {
return true;
}
if lower.is_empty() {
return false;
}
if has_non_app_document_extension(lower.as_str()) {
return true;
}
let has_docs = lower.contains("documentation") || lower.contains(" docs");
let has_sample = lower.contains("sample");
let has_tools_for = lower.contains("tools for");
let has_help_content = lower.contains("manual")
|| lower.contains("faq")
|| lower.contains("website")
|| lower.contains("web page")
|| lower.contains("webpage")
|| lower.contains("guide")
|| lower.contains("readme")
|| lower.contains("release notes")
|| lower.contains("changelog");
let has_apps = lower.contains(" app") || lower.contains("apps");
let has_platform =
lower.contains("desktop") || lower.contains("uwp") || lower.contains("winui");
(has_docs && has_apps)
|| (has_sample && (has_apps || has_platform))
|| (has_tools_for && has_apps && has_platform)
|| (has_help_content && (path_lower.ends_with(".lnk") || is_shell_appsfolder))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn has_non_app_document_extension(value: &str) -> bool {
let normalized = value.trim().to_ascii_lowercase();
if normalized.is_empty() {
return false;
}
[
".url", ".pdf", ".htm", ".html", ".xhtml", ".mht", ".mhtml", ".chm", ".txt", ".md", ".rtf",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".csv", ".xml", ".json", ".yaml",
".yml", ".ini", ".log", ".php",
]
.iter()
.any(|ext| normalized.ends_with(ext))
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn normalize_title_key(title: &str) -> String {
crate::model::normalize_for_search(title.trim())
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn shortcut_base_title_key(title: &str) -> String {
let trimmed = title.trim();
if trimmed.len() >= 4 && trimmed[trimmed.len() - 4..].eq_ignore_ascii_case(".lnk") {
normalize_title_key(&trimmed[..trimmed.len() - 4])
} else {
normalize_title_key(trimmed)
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn is_windows_shortcut_path(path: &str) -> bool {
let trimmed = path.trim();
trimmed.len() >= 4 && trimmed[trimmed.len() - 4..].eq_ignore_ascii_case(".lnk")
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn normalize_path_key(path: &str) -> String {
let trimmed = path.trim();
let mut normalized = String::with_capacity(trimmed.len());
for ch in trimmed.chars() {
if ch == '/' {
normalized.push('\\');
} else if ch.is_ascii_uppercase() {
normalized.push(ch.to_ascii_lowercase());
} else {
normalized.push(ch);
}
}
normalized
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn track_uninstall_title_suppression(
suppressed_uninstall_titles: &mut Vec<String>,
action_title: &str,
) {
let Some(target_title) = uninstall_target_title_from_action_title(action_title) else {
return;
};
if suppressed_uninstall_titles
.iter()
.any(|existing| existing.eq_ignore_ascii_case(target_title.as_str()))
{
return;
}
suppressed_uninstall_titles.push(target_title);
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn reconcile_suppressed_uninstall_titles(suppressed_uninstall_titles: &mut Vec<String>) {
if suppressed_uninstall_titles.is_empty() {
return;
}
suppressed_uninstall_titles.retain(|title| {
match crate::uninstall_registry::is_display_name_registered(title.as_str()) {
Ok(still_registered) => still_registered,
Err(error) => {
log_warn(&format!(
"[nex] uninstall suppression registry check failed for '{}': {}",
title, error
));
true
}
}
});
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn filter_suppressed_uninstall_results(
results: &mut Vec<crate::model::SearchItem>,
suppressed_uninstall_titles: &[String],
) {
if results.is_empty() || suppressed_uninstall_titles.is_empty() {
return;
}
let suppressed_keys: Vec<String> = suppressed_uninstall_titles
.iter()
.map(|title| crate::model::normalize_for_search(title.as_str()))
.filter(|key| !key.is_empty())
.collect();
if suppressed_keys.is_empty() {
return;
}
results.retain(|item| {
let title_key = if item.kind.eq_ignore_ascii_case("app") {
item.normalized_title().to_string()
} else if item.kind.eq_ignore_ascii_case("action")
&& item
.id
.starts_with(crate::uninstall_registry::ACTION_UNINSTALL_PREFIX)
{
uninstall_target_title_from_action_title(item.title.as_str())
.map(|title| crate::model::normalize_for_search(title.as_str()))
.unwrap_or_default()
} else {
return true;
};
if title_key.is_empty() {
return true;
}
!suppressed_keys
.iter()
.any(|suppressed| uninstall_title_matches(title_key.as_str(), suppressed.as_str()))
});
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn uninstall_target_title_from_action_title(action_title: &str) -> Option<String> {
let trimmed = action_title.trim();
if trimmed.len() <= "Uninstall ".len() {
return None;
}
if !trimmed
.get(.."Uninstall ".len())
.map(|prefix| prefix.eq_ignore_ascii_case("Uninstall "))
.unwrap_or(false)
{
return None;
}
let target = trimmed["Uninstall ".len()..].trim();
if target.is_empty() {
None
} else {
Some(target.to_string())
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn uninstall_title_matches(app_title_key: &str, suppressed_key: &str) -> bool {
if app_title_key.is_empty() || suppressed_key.is_empty() {
return false;
}
if app_title_key == suppressed_key {
return true;
}
if suppressed_key.len() >= 6
&& (app_title_key.starts_with(suppressed_key) || suppressed_key.starts_with(app_title_key))
{
return true;
}
suppressed_key.len() >= 10 && app_title_key.contains(suppressed_key)
}
#[cfg(target_os = "windows")]
fn overlay_subtitle(item: &crate::model::SearchItem, command_mode: bool) -> String {
if command_mode
&& item.kind.eq_ignore_ascii_case("action")
&& !item
.id
.starts_with(crate::uninstall_registry::ACTION_UNINSTALL_PREFIX)
{
return String::new();
}
if item.kind.eq_ignore_ascii_case("app") {
return item.subtitle.trim().to_string();
}
if item.kind.eq_ignore_ascii_case("action") {
if item.path.trim().is_empty() {
return "Nex action".to_string();
}
return item.path.trim().to_string();
}
abbreviate_path(&item.path)
}
#[cfg(target_os = "windows")]
fn abbreviate_path(path: &str) -> String {
let trimmed = path.trim();
if trimmed.is_empty() {
return String::new();
}
if trimmed.contains("://") {
return trimmed.to_string();
}
let normalized = trimmed.replace('/', "\\");
let mut parts: Vec<&str> = normalized.split('\\').filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return normalized;
}
if parts.first().is_some_and(|part| part.ends_with(':')) {
parts.remove(0);
}
if parts.is_empty() {
return String::new();
}
let tail_count = parts.len().min(3);
let joined_tail = parts[parts.len() - tail_count..].join("\\");
if parts.len() > 3 {
format!("...\\{joined_tail}")
} else {
joined_tail
}
}
#[cfg(target_os = "windows")]
fn set_idle_overlay_state(overlay: &NativeOverlayShell) {
overlay.clear_placeholder_hint();
overlay.set_results(&[], 0);
overlay.set_status_text("");
}
#[cfg(target_os = "windows")]
fn set_status_row_overlay_state(overlay: &NativeOverlayShell, message: &str) {
overlay.clear_placeholder_hint();
let rows = [OverlayRow {
role: OverlayRowRole::Status,
result_index: -1,
kind: "status".to_string(),
title: message.to_string(),
path: String::new(),
icon_path: String::new(),
}];
overlay.set_results(&rows, 0);
overlay.set_status_text("");
}
#[cfg(target_os = "windows")]
fn reset_overlay_session(
overlay: &NativeOverlayShell,
current_results: &mut Vec<crate::model::SearchItem>,
selected_index: &mut usize,
) {
overlay.clear_query_text();
current_results.clear();
*selected_index = 0;
set_idle_overlay_state(overlay);
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn next_selection_index(current: usize, len: usize, direction: i32) -> usize {
if len == 0 {
return 0;
}
let max = len - 1;
if direction < 0 {
current.saturating_sub(1)
} else if direction > 0 {
(current + 1).min(max)
} else {
current.min(max)
}
}
#[cfg(target_os = "windows")]
fn log_registration(registration: &HotkeyRegistration) {
match registration {
HotkeyRegistration::Native(id) => {
log_info(&format!(
"[nex] hotkey registered native_id={id}"
));
}
HotkeyRegistration::Noop(label) => {
log_info(&format!("[nex] hotkey registered noop={label}"));
}
}
}
#[cfg(target_os = "windows")]
struct SingleInstanceGuard {
handle: windows_sys::Win32::Foundation::HANDLE,
}
#[cfg(target_os = "windows")]
impl Drop for SingleInstanceGuard {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::Foundation::CloseHandle(self.handle);
}
}
}
#[cfg(target_os = "windows")]
fn acquire_single_instance_guard() -> Result<Option<SingleInstanceGuard>, String> {
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::System::Threading::CreateMutexW;
let mutex_name = to_wide("Local\\NexRuntimeSingleton");
let handle = unsafe { CreateMutexW(std::ptr::null(), 0, mutex_name.as_ptr()) };
if handle.is_null() {
let error = unsafe { GetLastError() };
return Err(format!("CreateMutexW failed with error {error}"));
}
let error = unsafe { GetLastError() };
if error == 183 {
unsafe {
windows_sys::Win32::Foundation::CloseHandle(handle);
}
return Ok(None);
}
Ok(Some(SingleInstanceGuard { handle }))
}
#[cfg(target_os = "windows")]
fn to_wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct ForegroundWindowSnapshot {
class_name: String,
process_name: String,
process_path: String,
covers_monitor: bool,
has_standard_frame: bool,
maximized: bool,
}
#[cfg(target_os = "windows")]
fn toggle_game_mode_from_tray(overlay: &NativeOverlayShell, runtime_config: &mut Config) {
let previous = runtime_config.game_mode_enabled;
let next = !previous;
runtime_config.game_mode_enabled = next;
overlay.set_game_mode_enabled(next);
if let Err(error) = config::write_user_template(runtime_config, &runtime_config.config_path) {
runtime_config.game_mode_enabled = previous;
overlay.set_game_mode_enabled(previous);
log_warn(&format!(
"[nex] failed to persist game mode toggle: {error}"
));
overlay.set_status_text("Could not update Game Mode setting");
return;
}
log_info(&format!(
"[nex] game mode updated from tray: enabled={next}"
));
overlay.set_status_text(if next {
"Game Mode enabled"
} else {
"Game Mode disabled"
});
}
#[cfg(target_os = "windows")]
fn should_suppress_hotkey_for_game_mode(cfg: &Config) -> bool {
if !cfg.game_mode_enabled {
return false;
}
collect_foreground_window_snapshot()
.is_some_and(|snapshot| should_block_hotkey_for_foreground_window(&snapshot))
}
#[cfg(target_os = "windows")]
fn collect_foreground_window_snapshot() -> Option<ForegroundWindowSnapshot> {
use windows_sys::Win32::Foundation::{CloseHandle, RECT};
use windows_sys::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST,
};
use windows_sys::Win32::System::Threading::{
OpenProcess, QueryFullProcessImageNameW, PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetClassNameW, GetForegroundWindow, GetWindowLongPtrW, GetWindowPlacement, GetWindowRect,
GetWindowThreadProcessId, IsIconic, IsWindowVisible, GWL_STYLE, SW_SHOWMAXIMIZED,
WINDOWPLACEMENT, WS_CAPTION, WS_MAXIMIZE, WS_SYSMENU, WS_THICKFRAME,
};
let foreground = unsafe { GetForegroundWindow() };
if foreground.is_null() {
return None;
}
if unsafe { IsWindowVisible(foreground) } == 0 || unsafe { IsIconic(foreground) } != 0 {
return None;
}
let mut class_buf = [0u16; 128];
let class_len =
unsafe { GetClassNameW(foreground, class_buf.as_mut_ptr(), class_buf.len() as i32) };
let class_name = if class_len > 0 {
String::from_utf16_lossy(&class_buf[..class_len as usize])
} else {
String::new()
};
if is_shell_surface_class_name(&class_name) {
return None;
}
let monitor = unsafe { MonitorFromWindow(foreground, MONITOR_DEFAULTTONEAREST) };
if monitor.is_null() {
return None;
}
let mut monitor_info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
rcMonitor: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
rcWork: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
dwFlags: 0,
};
if unsafe { GetMonitorInfoW(monitor, &mut monitor_info as *mut MONITORINFO) } == 0 {
return None;
}
let mut rect = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
if unsafe { GetWindowRect(foreground, &mut rect as *mut RECT) } == 0 {
return None;
}
let fuzz = 2;
let covers_monitor = rect.left <= monitor_info.rcMonitor.left + fuzz
&& rect.top <= monitor_info.rcMonitor.top + fuzz
&& rect.right >= monitor_info.rcMonitor.right - fuzz
&& rect.bottom >= monitor_info.rcMonitor.bottom - fuzz;
let style = unsafe { GetWindowLongPtrW(foreground, GWL_STYLE) as u32 };
let has_standard_frame =
style & ((WS_CAPTION | WS_THICKFRAME | WS_SYSMENU) as u32) != 0;
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
flags: 0,
showCmd: 0,
ptMinPosition: windows_sys::Win32::Foundation::POINT { x: 0, y: 0 },
ptMaxPosition: windows_sys::Win32::Foundation::POINT { x: 0, y: 0 },
rcNormalPosition: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
};
let placement_reports_maximized =
unsafe { GetWindowPlacement(foreground, &mut placement as *mut WINDOWPLACEMENT) } != 0
&& placement.showCmd == SW_SHOWMAXIMIZED as u32;
let maximized = placement_reports_maximized || (style & (WS_MAXIMIZE as u32) != 0);
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(foreground, &mut pid as *mut u32);
}
let mut process_path = String::new();
let mut process_name = String::new();
if pid != 0 {
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if !process.is_null() {
let mut buffer = vec![0u16; 1024];
let mut length = buffer.len() as u32;
let ok = unsafe {
QueryFullProcessImageNameW(process, 0, buffer.as_mut_ptr(), &mut length as *mut u32)
};
if ok != 0 && length > 0 {
process_path = String::from_utf16_lossy(&buffer[..length as usize]);
process_name = std::path::Path::new(&process_path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_string();
}
unsafe {
CloseHandle(process);
}
}
}
Some(ForegroundWindowSnapshot {
class_name,
process_name,
process_path,
covers_monitor,
has_standard_frame,
maximized,
})
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn should_block_hotkey_for_foreground_window(snapshot: &ForegroundWindowSnapshot) -> bool {
if !snapshot.covers_monitor {
return false;
}
if snapshot.maximized && snapshot.has_standard_frame {
return false;
}
let process_name = snapshot.process_name.trim().to_ascii_lowercase();
let process_path = snapshot.process_path.trim().to_ascii_lowercase();
if is_known_non_game_process(&process_name) || is_known_non_game_path(&process_path) {
return false;
}
if is_known_game_process(&process_name) || is_likely_game_path(&process_path) {
return true;
}
!snapshot.has_standard_frame
}
#[allow(dead_code)]
fn is_shell_surface_class_name(class_name: &str) -> bool {
matches!(
class_name.trim().to_ascii_lowercase().as_str(),
"progman" | "workerw" | "shell_traywnd"
)
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn is_known_non_game_process(process_name: &str) -> bool {
matches!(
process_name,
""
| "explorer.exe"
| "taskmgr.exe"
| "chrome.exe"
| "msedge.exe"
| "firefox.exe"
| "waterfox.exe"
| "code.exe"
| "devenv.exe"
| "wezterm-gui.exe"
| "windowsterminal.exe"
| "powershell.exe"
| "pwsh.exe"
| "cmd.exe"
| "notepad.exe"
| "notepad++.exe"
| "vlc.exe"
| "mpv.exe"
| "obs64.exe"
| "applicationframehost.exe"
| "searchhost.exe"
| "startmenuexperiencehost.exe"
| "lockapp.exe"
)
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn is_known_non_game_path(process_path: &str) -> bool {
process_path.contains("\\microsoft\\edge\\application\\")
|| process_path.contains("\\google\\chrome\\application\\")
|| process_path.contains("\\mozilla firefox\\")
|| process_path.contains("\\microsoft\\windowsapps\\")
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn is_known_game_process(process_name: &str) -> bool {
process_name.contains("valorant")
|| process_name == "cs2.exe"
|| process_name == "csgo.exe"
|| process_name == "cod.exe"
|| process_name == "modernwarfare.exe"
|| process_name.ends_with("-shipping.exe")
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn is_likely_game_path(process_path: &str) -> bool {
process_path.contains("\\steamapps\\common\\")
|| process_path.contains("\\riot games\\")
|| process_path.contains("\\epic games\\")
|| process_path.contains("\\battle.net\\")
|| process_path.contains("\\blizzard entertainment\\")
|| process_path.contains("\\ubisoft\\")
|| process_path.contains("\\rockstar games\\")
|| process_path.contains("\\gog galaxy\\games\\")
|| process_path.contains("\\ea games\\")
|| process_path.contains("\\electronic arts\\")
}
#[cfg(target_os = "windows")]
fn start_background_index_refresh(
config: &Config,
initial_cache_empty: bool,
startup_started_at: Instant,
) -> BackgroundIndexRefresh {
let completed = Arc::new(AtomicBool::new(false));
let result = Arc::new(Mutex::new(None));
let completed_worker = completed.clone();
let result_worker = result.clone();
let worker_config = config.clone();
std::thread::spawn(move || {
let outcome = CoreService::new(worker_config)
.map(|service| service.with_runtime_providers())
.and_then(|service| service.rebuild_index_incremental_with_report())
.map_err(|error| format!("background indexing failed: {error}"));
let mut slot = match result_worker.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*slot = Some(outcome);
completed_worker.store(true, Ordering::Release);
});
BackgroundIndexRefresh {
completed,
result,
cache_applied: false,
initial_cache_empty,
pending_discovery_reindex: false,
pending_discovery_reindex_due_at: None,
pending_discovery_reindex_requests: 0,
ready_notice_pending: false,
started_at: Instant::now(),
startup_started_at,
}
}
#[cfg(target_os = "windows")]
fn maybe_apply_background_index_refresh(
service: &CoreService,
state: &mut BackgroundIndexRefresh,
runtime_config: &Config,
) {
if state.cache_applied {
maybe_start_queued_discovery_reindex(service, state, runtime_config);
return;
}
if !state.completed.load(Ordering::Acquire) {
return;
}
let outcome = {
let mut slot = match state.result.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
slot.take()
};
match outcome {
Some(Ok(report)) => {
let elapsed_ms = state.started_at.elapsed().as_millis();
let startup_elapsed_ms = state.startup_started_at.elapsed().as_millis();
log_info(&format!(
"[nex] startup_phase phase=indexing_completed elapsed_ms={} worker_elapsed_ms={} indexed_items={} discovered={} upserted={} removed={}",
startup_elapsed_ms,
elapsed_ms,
report.indexed_total,
report.discovered_total,
report.upserted_total,
report.removed_total
));
match service.reload_cache_from_store() {
Ok(cached_items) => {
log_info(&format!(
"[nex] startup indexed_items={} discovered={} upserted={} removed={} elapsed_ms={} cached_items={}",
report.indexed_total,
report.discovered_total,
report.upserted_total,
report.removed_total,
elapsed_ms,
cached_items
));
log_info(&format!(
"[nex] startup_phase phase=cache_applied elapsed_ms={} cached_items={} initial_cache_empty={}",
startup_elapsed_ms,
cached_items,
state.initial_cache_empty
));
for provider in &report.providers {
log_info(&format!(
"[nex] index_provider name={} discovered={} upserted={} removed={} skipped={} elapsed_ms={}",
provider.provider,
provider.discovered,
provider.upserted,
provider.removed,
provider.skipped,
provider.elapsed_ms
));
}
}
Err(error) => {
log_warn(&format!(
"[nex] background indexing cache refresh failed: {error}"
));
}
}
if state.initial_cache_empty {
state.ready_notice_pending = true;
}
}
Some(Err(error)) => {
log_warn(&format!("[nex] {error}"));
}
None => {
log_warn("[nex] background indexing completed without result");
}
}
state.cache_applied = true;
if state.pending_discovery_reindex {
log_info("[nex] discovery settings queued during indexing; pending reindex remains scheduled");
maybe_start_queued_discovery_reindex(service, state, runtime_config);
}
}
#[cfg(target_os = "windows")]
fn should_show_indexing_status(state: &BackgroundIndexRefresh) -> bool {
state.initial_cache_empty && !state.cache_applied
}
#[cfg(target_os = "windows")]
fn queue_discovery_reindex_after_active_index(state: &mut BackgroundIndexRefresh) {
state.pending_discovery_reindex = true;
state.pending_discovery_reindex_requests =
state.pending_discovery_reindex_requests.saturating_add(1);
state.pending_discovery_reindex_due_at = Some(
Instant::now() + Duration::from_millis(QUEUED_DISCOVERY_REINDEX_DEBOUNCE_MS),
);
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn queued_discovery_reindex_is_due(
cache_applied: bool,
pending: bool,
due_at: Option<Instant>,
now: Instant,
) -> bool {
cache_applied && pending && due_at.is_some_and(|due| now >= due)
}
#[cfg(target_os = "windows")]
fn maybe_start_queued_discovery_reindex(
service: &CoreService,
state: &mut BackgroundIndexRefresh,
runtime_config: &Config,
) {
if !queued_discovery_reindex_is_due(
state.cache_applied,
state.pending_discovery_reindex,
state.pending_discovery_reindex_due_at,
Instant::now(),
) {
return;
}
let request_count = state.pending_discovery_reindex_requests.max(1);
let startup_started_at = state.startup_started_at;
log_info(&format!(
"[nex] discovery settings queued during indexing; starting debounced reindex requests={} debounce_ms={}",
request_count,
QUEUED_DISCOVERY_REINDEX_DEBOUNCE_MS
));
log_info(&format!(
"[nex] startup_phase phase=indexing_started elapsed_ms={} initial_cache_empty=false cached_items={}",
startup_started_at.elapsed().as_millis(),
service.cached_items_len()
));
*state = start_background_index_refresh(runtime_config, false, startup_started_at);
}
#[cfg(target_os = "windows")]
fn maybe_show_background_index_ready_notice(
overlay: &NativeOverlayShell,
state: &mut BackgroundIndexRefresh,
) {
if !state.ready_notice_pending || !overlay.is_visible() {
return;
}
if !overlay.query_text().trim().is_empty() {
return;
}
set_idle_overlay_state(overlay);
overlay.show_placeholder_hint(STATUS_ROW_TYPE_TO_SEARCH);
overlay.set_status_text(STATUS_TEXT_INDEX_READY);
state.ready_notice_pending = false;
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
#[cfg_attr(not(test), allow(dead_code))]
fn search_overlay_results(
service: &CoreService,
cfg: &Config,
plugins: &PluginRegistry,
parsed_query: &ParsedQuery,
result_limit: usize,
) -> Result<Vec<crate::model::SearchItem>, String> {
let mut session = OverlaySearchSession::default();
search_overlay_results_with_session(
service,
cfg,
plugins,
parsed_query,
result_limit,
&mut session,
)
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn search_overlay_results_with_session(
service: &CoreService,
cfg: &Config,
plugins: &PluginRegistry,
parsed_query: &ParsedQuery,
result_limit: usize,
session: &mut OverlaySearchSession,
) -> Result<Vec<crate::model::SearchItem>, String> {
if result_limit == 0 {
return Ok(Vec::new());
}
let filter = build_search_filter(cfg, parsed_query);
let text_query = parsed_query.free_text.trim();
let normalized_query = crate::model::normalize_for_search(text_query);
if should_skip_non_searchable_query(parsed_query, &normalized_query) {
log_info(&format!(
"[nex] query_guard skip=non_searchable_symbol_only q=\"{}\"",
sanitize_query_for_profile_log(parsed_query.raw.as_str())
));
session.clear();
return Ok(Vec::new());
}
let cache_key = final_query_cache_key(parsed_query, &filter, &normalized_query, result_limit);
if let Some(cached) = cached_final_query_results(session, &cache_key) {
return Ok(cached);
}
let candidate_limit = candidate_limit_for_query(
result_limit,
&filter,
&normalized_query,
parsed_query.command_mode,
);
let base_indexed_seed_limit = indexed_seed_limit(candidate_limit, normalized_query.len());
let seed_cap = (cfg.index_max_items_per_query_seed as usize).max(candidate_limit);
let indexed_seed_limit = adaptive_indexed_seed_limit(
session,
candidate_limit,
normalized_query.len(),
base_indexed_seed_limit,
)
.min(seed_cap);
let short_query_app_bias =
should_use_short_query_app_mode(parsed_query, &filter, &normalized_query);
let mut indexed_filter = filter.clone();
if short_query_app_bias {
indexed_filter.mode = crate::config::SearchMode::Apps;
}
let search_started = Instant::now();
let mut merged = Vec::new();
let indexed_started = Instant::now();
let mut indexed_cache_hit = false;
let prefix_cache_eligible = is_prefix_cache_eligible_query(parsed_query, short_query_app_bias);
let indexed_seed_items = if let Some(cache) =
session.indexed_prefix_cache.as_ref().filter(|cache| {
can_use_indexed_prefix_cache(
cache,
prefix_cache_eligible,
&normalized_query,
&indexed_filter,
)
}) {
indexed_cache_hit = true;
crate::search::search_with_filter(
&cache.seed_items,
text_query,
indexed_seed_limit,
&indexed_filter,
)
} else {
service
.search_with_filter_uncapped(text_query, indexed_seed_limit, &indexed_filter)
.map_err(|error| format!("indexed search failed: {error}"))?
};
let indexed_ms = indexed_started.elapsed().as_millis();
if !indexed_cache_hit {
record_indexed_latency_sample(session, indexed_ms);
}
let indexed_count = indexed_seed_items.len();
merged.extend(indexed_seed_items.iter().take(candidate_limit).cloned());
if prefix_cache_eligible && normalized_query.len() >= INDEXED_PREFIX_CACHE_MIN_QUERY_LEN {
session.indexed_prefix_cache = Some(IndexedPrefixCache {
normalized_query: normalized_query.clone(),
indexed_filter: indexed_filter.clone(),
seed_items: indexed_seed_items,
});
} else {
session.clear();
}
let mut provider_ms = 0_u128;
let mut provider_count = 0_usize;
if !short_query_app_bias {
let provider_started = Instant::now();
let provider_results = crate::search::search_with_filter(
&plugins.provider_items,
text_query,
candidate_limit,
&filter,
);
provider_ms = provider_started.elapsed().as_millis();
provider_count = provider_results.len();
merged.extend(provider_results);
}
let actions_started = Instant::now();
let mut action_items =
search_actions_with_mode(text_query, candidate_limit, parsed_query.command_mode, cfg);
let built_in_actions_count = action_items.len();
let mut plugin_action_count = 0_usize;
if !plugins.action_items.is_empty() {
let plugin_actions = crate::search::search_with_filter(
&plugins.action_items,
text_query,
candidate_limit,
&SearchFilter {
mode: crate::config::SearchMode::Actions,
..SearchFilter::default()
},
);
plugin_action_count = plugin_actions.len();
action_items.extend(plugin_actions);
}
let action_results =
crate::search::search_with_filter(&action_items, text_query, candidate_limit, &filter);
let actions_ms = actions_started.elapsed().as_millis();
let action_count = action_results.len();
merged.extend(action_results);
let mut clipboard_ms = 0_u128;
let mut clipboard_count = 0_usize;
if !short_query_app_bias {
let clipboard_started = Instant::now();
let clipboard_results =
clipboard_history::search_history(cfg, text_query, &filter, candidate_limit.min(120));
clipboard_ms = clipboard_started.elapsed().as_millis();
clipboard_count = clipboard_results.len();
merged.extend(clipboard_results);
}
let rank_started = Instant::now();
let ranked = crate::search::search_with_filter(&merged, text_query, result_limit, &filter);
let rank_ms = rank_started.elapsed().as_millis();
let total_ms = search_started.elapsed().as_millis();
if total_ms >= QUERY_PROFILE_LOG_THRESHOLD_MS {
log_info(&format!(
"[nex] query_profile q=\"{}\" mode={} candidate_limit={} indexed_seed_limit={} short_app_bias={} indexed_cache_hit={} indexed_count={} indexed_ms={} provider_count={} provider_ms={} action_count={} action_ms={} built_in_actions={} plugin_actions={} clipboard_count={} clipboard_ms={} rank_ms={} total_ms={}",
sanitize_query_for_profile_log(text_query),
format!("{:?}", filter.mode).to_ascii_lowercase(),
candidate_limit,
indexed_seed_limit,
short_query_app_bias,
indexed_cache_hit,
indexed_count,
indexed_ms,
provider_count,
provider_ms,
action_count,
actions_ms,
built_in_actions_count,
plugin_action_count,
clipboard_count,
clipboard_ms,
rank_ms,
total_ms
));
}
store_final_query_results(session, cache_key, ranked.as_slice());
Ok(ranked)
}
fn build_search_filter(cfg: &Config, parsed_query: &ParsedQuery) -> SearchFilter {
let mode = resolved_mode_for_query(cfg, parsed_query);
SearchFilter {
mode,
kind_filter: parsed_query.kind_filter.clone(),
extension_filter: parsed_query.extension_filter.clone(),
include_files: cfg.show_files,
include_folders: cfg.show_folders,
include_groups: parsed_query.include_groups.clone(),
exclude_terms: parsed_query.exclude_terms.clone(),
modified_within: parsed_query.modified_within,
created_within: parsed_query.created_within,
}
}
fn resolved_mode_for_query(cfg: &Config, parsed_query: &ParsedQuery) -> crate::config::SearchMode {
let mut mode = parsed_query
.mode_override
.unwrap_or(cfg.search_mode_default);
if parsed_query.command_mode {
mode = crate::config::SearchMode::Actions;
}
mode
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn should_use_short_query_app_mode(
parsed_query: &ParsedQuery,
filter: &SearchFilter,
normalized_query: &str,
) -> bool {
if normalized_query.is_empty() || normalized_query.len() > SHORT_QUERY_APP_BIAS_MAX_LEN {
return false;
}
if parsed_query.command_mode {
return false;
}
if filter.mode != crate::config::SearchMode::All {
return false;
}
parsed_query.kind_filter.is_none()
&& parsed_query.extension_filter.is_none()
&& parsed_query.exclude_terms.is_empty()
&& parsed_query.modified_within.is_none()
&& parsed_query.created_within.is_none()
}
fn should_skip_non_searchable_query(parsed_query: &ParsedQuery, normalized_query: &str) -> bool {
if !normalized_query.is_empty() {
return false;
}
if parsed_query.command_mode {
return false;
}
if parsed_query.mode_override.is_some() {
return false;
}
parsed_query.kind_filter.is_none()
&& parsed_query.extension_filter.is_none()
&& parsed_query.include_groups.is_empty()
&& parsed_query.exclude_terms.is_empty()
&& parsed_query.modified_within.is_none()
&& parsed_query.created_within.is_none()
}
#[cfg_attr(not(any(test, target_os = "windows")), allow(dead_code))]
fn result_limit_for_query(base_limit: usize, parsed_query: &ParsedQuery) -> usize {
if base_limit == 0 {
return 0;
}
if parsed_query.command_mode
&& crate::uninstall_registry::has_uninstall_intent(parsed_query.free_text.as_str())
{
return base_limit.max(UNINSTALL_QUERY_RESULT_LIMIT);
}
base_limit
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn maybe_expand_uninstall_quick_shortcut(query: &str, last_query: &str) -> Option<String> {
let raw = query.trim_start();
let remainder = raw.strip_prefix('>')?;
if remainder.eq_ignore_ascii_case("u") {
let last_trimmed = last_query.trim();
if last_trimmed.is_empty() || last_trimmed == ">" {
return Some(">u ".to_string());
}
}
None
}
#[cfg(target_os = "windows")]
fn apply_query_change(
mut query: String,
overlay: &NativeOverlayShell,
service: &CoreService,
runtime_config: &Config,
plugin_registry: &PluginRegistry,
max_results: usize,
background_index_refresh: &BackgroundIndexRefresh,
search_session: &mut OverlaySearchSession,
pending_uninstall_confirmation: &mut Option<PendingUninstallConfirmation>,
suppressed_uninstall_titles: &[String],
current_results: &mut Vec<crate::model::SearchItem>,
selected_index: &mut usize,
last_query: &mut String,
) {
*pending_uninstall_confirmation = None;
if let Some(expanded) = maybe_expand_uninstall_quick_shortcut(&query, last_query.as_str()) {
overlay.set_query_text(&expanded);
query = expanded;
}
let trimmed = query.trim();
if trimmed.is_empty() {
current_results.clear();
*selected_index = 0;
last_query.clear();
search_session.clear();
*pending_uninstall_confirmation = None;
set_idle_overlay_state(overlay);
return;
}
if trimmed == last_query {
return;
}
*last_query = trimmed.to_string();
let parsed_query = ParsedQuery::parse(trimmed, runtime_config.search_dsl_enabled);
let query_result_limit = result_limit_for_query(max_results, &parsed_query);
match search_overlay_results_with_session(
service,
runtime_config,
plugin_registry,
&parsed_query,
query_result_limit,
search_session,
) {
Ok(mut results) => {
dedupe_overlay_results(&mut results);
if !suppressed_uninstall_titles.is_empty() {
filter_suppressed_uninstall_results(&mut results, suppressed_uninstall_titles);
}
*current_results = results;
*selected_index = 0;
if current_results.is_empty() {
if should_show_indexing_status(background_index_refresh) {
set_status_row_overlay_state(overlay, STATUS_ROW_INDEXING);
} else {
set_status_row_overlay_state(
overlay,
if parsed_query.command_mode {
STATUS_ROW_NO_COMMAND_RESULTS
} else {
STATUS_ROW_NO_RESULTS
},
);
}
} else {
let rows = overlay_rows(current_results, parsed_query.command_mode);
overlay.set_results(&rows, *selected_index);
}
}
Err(error) => {
current_results.clear();
*selected_index = 0;
search_session.clear();
overlay.set_results(&[], 0);
overlay.set_status_text(&format!("Search error: {error}"));
}
}
}
#[cfg(target_os = "windows")]
fn config_file_modified_time(path: &std::path::Path) -> Option<SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
#[cfg(target_os = "windows")]
fn maybe_apply_runtime_config_reload(
overlay: &NativeOverlayShell,
service: &CoreService,
runtime_config: &mut Config,
plugin_registry: &mut PluginRegistry,
search_session: &mut OverlaySearchSession,
pending_uninstall_confirmation: &mut Option<PendingUninstallConfirmation>,
max_results: &mut usize,
watcher: &mut RuntimeConfigWatcher,
background_index_refresh: &mut BackgroundIndexRefresh,
) {
if watcher.last_checked.elapsed() < CONFIG_RELOAD_POLL_INTERVAL {
return;
}
watcher.last_checked = Instant::now();
let modified = config_file_modified_time(watcher.path.as_path());
if modified == watcher.last_modified {
return;
}
watcher.last_modified = modified;
match config::load(Some(watcher.path.as_path())) {
Ok(next_config) => {
let previous = runtime_config.clone();
let hotkey_changed = next_config.hotkey != previous.hotkey;
let index_db_path_changed = next_config.index_db_path != previous.index_db_path;
let discovery_config_changed = next_config.discovery_roots != previous.discovery_roots
|| next_config.discovery_exclude_roots != previous.discovery_exclude_roots
|| next_config.windows_search_enabled != previous.windows_search_enabled
|| next_config.windows_search_fallback_filesystem
!= previous.windows_search_fallback_filesystem
|| next_config.show_files != previous.show_files
|| next_config.show_folders != previous.show_folders
|| next_config.index_max_items_total != previous.index_max_items_total
|| next_config.index_max_items_per_root != previous.index_max_items_per_root
|| next_config.index_max_items_per_query_seed
!= previous.index_max_items_per_query_seed;
let mut discovery_reindex_queued = false;
*runtime_config = next_config;
*max_results = runtime_config.max_results as usize;
overlay.set_performance_tuning(
runtime_config.idle_cache_trim_ms,
runtime_config.active_memory_target_mb,
);
overlay.set_game_mode_enabled(runtime_config.game_mode_enabled);
*plugin_registry = PluginRegistry::load_from_config(runtime_config);
for warning in &plugin_registry.load_warnings {
log_warn(&format!("[nex] plugin_warning {warning}"));
}
search_session.clear();
*pending_uninstall_confirmation = None;
if hotkey_changed {
log_warn(&format!(
"[nex] config hotkey changed ({} -> {}), restart required to apply",
previous.hotkey, runtime_config.hotkey
));
}
if index_db_path_changed {
log_warn(
"[nex] config index_db_path changed; restart required to apply",
);
}
if discovery_config_changed {
if let Err(error) = service.reconfigure_runtime_providers(runtime_config) {
log_warn(&format!(
"[nex] provider reconfigure failed after config reload: {error}"
));
} else {
if background_index_refresh.cache_applied {
*background_index_refresh =
start_background_index_refresh(
runtime_config,
false,
background_index_refresh.startup_started_at,
);
log_info("[nex] discovery settings changed; background reindex started");
} else {
queue_discovery_reindex_after_active_index(background_index_refresh);
discovery_reindex_queued = true;
log_info(&format!(
"[nex] discovery settings changed while indexing is active; reindex queued debounce_ms={} requests={}",
QUEUED_DISCOVERY_REINDEX_DEBOUNCE_MS,
background_index_refresh.pending_discovery_reindex_requests
));
}
}
}
log_info(&format!(
"[nex] config reloaded max_results={} mode={:?} show_files={} show_folders={} game_mode={} dsl={} clipboard={} uninstall_actions={} plugins_enabled={} plugins_actions={} index_caps_total={} index_caps_per_root={} index_seed_cap={}",
runtime_config.max_results,
runtime_config.search_mode_default,
runtime_config.show_files,
runtime_config.show_folders,
runtime_config.game_mode_enabled,
runtime_config.search_dsl_enabled,
runtime_config.clipboard_enabled,
runtime_config.uninstall_actions_enabled,
runtime_config.plugins_enabled,
plugin_registry.action_items.len(),
runtime_config.index_max_items_total,
runtime_config.index_max_items_per_root,
runtime_config.index_max_items_per_query_seed,
));
if discovery_config_changed {
if discovery_reindex_queued {
overlay.set_status_text("Discovery settings queued; reindex starts after debounce");
} else {
overlay.set_status_text("Discovery settings updated; reindexing...");
}
} else if index_db_path_changed {
overlay.set_status_text("Restart required to apply index path changes");
} else if hotkey_changed {
overlay.set_status_text("Restart required to apply hotkey changes");
} else {
overlay.set_status_text("Settings applied");
}
}
Err(error) => {
log_warn(&format!(
"[nex] config reload skipped due to invalid config: {error}"
));
}
}
}
#[cfg(target_os = "windows")]
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))]
fn uninstall_confirmation_results(
uninstall_action: &crate::model::SearchItem,
) -> Vec<crate::model::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![
crate::model::SearchItem::new(
ACTION_UNINSTALL_CONFIRM_ID,
"action",
confirm_title.as_str(),
"Open app uninstaller",
),
crate::model::SearchItem::new(
ACTION_UNINSTALL_CANCEL_ID,
"action",
"Cancel",
"Return to previous results",
),
]
}
fn candidate_limit_for_query(
result_limit: usize,
filter: &SearchFilter,
normalized_query: &str,
command_mode: bool,
) -> usize {
if result_limit == 0 {
return 0;
}
let base = result_limit.saturating_mul(6).max(60);
if command_mode || filter.mode == crate::config::SearchMode::Actions {
return result_limit
.saturating_mul(4)
.max(48)
.min(160)
.max(result_limit);
}
match normalized_query.len() {
0 => result_limit
.saturating_mul(2)
.max(24)
.min(64)
.max(result_limit),
1 => match filter.mode {
crate::config::SearchMode::All => result_limit
.saturating_mul(3)
.max(45)
.min(96)
.max(result_limit),
crate::config::SearchMode::Files => result_limit
.saturating_mul(5)
.max(70)
.min(200)
.max(result_limit),
_ => result_limit
.saturating_mul(4)
.max(56)
.min(180)
.max(result_limit),
},
2 => match filter.mode {
crate::config::SearchMode::All => result_limit
.saturating_mul(4)
.max(56)
.min(140)
.max(result_limit),
crate::config::SearchMode::Files => result_limit
.saturating_mul(5)
.max(70)
.min(200)
.max(result_limit),
_ => result_limit
.saturating_mul(4)
.max(56)
.min(180)
.max(result_limit),
},
_ => base.min(280).max(result_limit),
}
}
fn indexed_seed_limit(candidate_limit: usize, normalized_query_len: usize) -> usize {
let multiplier = match normalized_query_len {
0 | 1 => 4,
2 => 2,
_ => 2,
};
candidate_limit.saturating_mul(multiplier).clamp(
INDEXED_PREFIX_CACHE_MIN_SEED_LIMIT,
INDEXED_PREFIX_CACHE_MAX_SEED_LIMIT,
)
}
fn adaptive_indexed_seed_limit(
session: &OverlaySearchSession,
candidate_limit: usize,
normalized_query_len: usize,
base_seed_limit: usize,
) -> usize {
let mut samples: Vec<u128> = session.indexed_latency_ms.iter().copied().collect();
if samples.len() < 6 {
return base_seed_limit;
}
let p95 = percentile_u128(&mut samples, 0.95);
let scaled = if p95 >= 160 {
(base_seed_limit.saturating_mul(60)) / 100
} else if p95 >= 120 {
(base_seed_limit.saturating_mul(72)) / 100
} else if p95 >= 95 {
(base_seed_limit.saturating_mul(84)) / 100
} else if p95 <= 50 && normalized_query_len >= 3 {
(base_seed_limit.saturating_mul(108)) / 100
} else {
base_seed_limit
};
let minimum = candidate_limit.max(INDEXED_PREFIX_CACHE_MIN_SEED_LIMIT / 2);
scaled.clamp(minimum, INDEXED_PREFIX_CACHE_MAX_SEED_LIMIT)
}
fn record_indexed_latency_sample(session: &mut OverlaySearchSession, indexed_ms: u128) {
session.indexed_latency_ms.push_back(indexed_ms);
while session.indexed_latency_ms.len() > ADAPTIVE_INDEXED_LATENCY_WINDOW {
session.indexed_latency_ms.pop_front();
}
}
fn final_query_cache_key(
parsed_query: &ParsedQuery,
filter: &SearchFilter,
normalized_query: &str,
result_limit: usize,
) -> String {
format!(
"q={};mode={:?};kind={};ext={};include={};exclude={};modified={:?};created={:?};cmd={};limit={}",
normalized_query,
filter.mode,
filter.kind_filter.as_deref().unwrap_or("-"),
filter.extension_filter.as_deref().unwrap_or("-"),
encode_term_groups(&filter.include_groups),
filter.exclude_terms.join(","),
filter.modified_within,
filter.created_within,
parsed_query.command_mode,
result_limit
)
}
fn encode_term_groups(groups: &[Vec<String>]) -> String {
if groups.is_empty() {
return "-".to_string();
}
groups
.iter()
.map(|group| group.join("+"))
.collect::<Vec<String>>()
.join("|")
}
fn cached_final_query_results(
session: &mut OverlaySearchSession,
key: &str,
) -> Option<Vec<crate::model::SearchItem>> {
let cached = session.final_query_cache.get(key).cloned()?;
if let Some(position) = session
.final_query_cache_lru
.iter()
.position(|entry| entry == key)
{
session.final_query_cache_lru.remove(position);
}
session.final_query_cache_lru.push_back(key.to_string());
Some(cached)
}
fn store_final_query_results(
session: &mut OverlaySearchSession,
key: String,
results: &[crate::model::SearchItem],
) {
if results.is_empty() {
return;
}
session
.final_query_cache
.insert(key.clone(), results.to_vec());
if let Some(position) = session
.final_query_cache_lru
.iter()
.position(|entry| entry == &key)
{
session.final_query_cache_lru.remove(position);
}
session.final_query_cache_lru.push_back(key);
while session.final_query_cache.len() > FINAL_QUERY_CACHE_MAX_ENTRIES {
let Some(oldest) = session.final_query_cache_lru.pop_front() else {
break;
};
session.final_query_cache.remove(&oldest);
}
}
fn can_use_indexed_prefix_cache(
cache: &IndexedPrefixCache,
prefix_cache_eligible: bool,
normalized_query: &str,
indexed_filter: &SearchFilter,
) -> bool {
if !prefix_cache_eligible {
return false;
}
if cache.seed_items.is_empty() || cache.normalized_query.is_empty() {
return false;
}
if !indexed_filter_matches_for_prefix_cache(&cache.indexed_filter, indexed_filter) {
return false;
}
normalized_query.len() > cache.normalized_query.len()
&& normalized_query.starts_with(&cache.normalized_query)
}
fn indexed_filter_matches_for_prefix_cache(a: &SearchFilter, b: &SearchFilter) -> bool {
a.mode == b.mode
&& a.kind_filter == b.kind_filter
&& a.extension_filter == b.extension_filter
&& a.modified_within == b.modified_within
&& a.created_within == b.created_within
}
fn is_prefix_cache_eligible_query(parsed_query: &ParsedQuery, short_query_app_bias: bool) -> bool {
if short_query_app_bias || parsed_query.command_mode {
return false;
}
if parsed_query.mode_override.is_some()
|| parsed_query.kind_filter.is_some()
|| parsed_query.extension_filter.is_some()
|| !parsed_query.exclude_terms.is_empty()
|| parsed_query.modified_within.is_some()
|| parsed_query.created_within.is_some()
{
return false;
}
if parsed_query.free_text.trim().is_empty() {
return false;
}
parsed_query.raw.trim() == parsed_query.free_text.trim()
}
fn sanitize_query_for_profile_log(query: &str) -> String {
const MAX_QUERY_LOG_CHARS: usize = 48;
let trimmed = query.trim();
if trimmed.is_empty() {
return "-".to_string();
}
let mut cleaned = String::new();
for ch in trimmed.chars().take(MAX_QUERY_LOG_CHARS) {
if ch.is_control() {
cleaned.push(' ');
} else {
cleaned.push(ch);
}
}
cleaned.trim().to_string()
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
fn launch_overlay_selection(
service: &CoreService,
cfg: &Config,
plugins: &PluginRegistry,
results: &[crate::model::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))]
fn execute_action_selection(
service: &CoreService,
cfg: &Config,
plugins: &PluginRegistry,
selected: &crate::model::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 = 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 => 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),
}
}
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(())
}
}
}
fn configure_stdio_logging(options: RuntimeOptions) {
let suppress_from_env = env_var_with_legacy("NEX_SUPPRESS_STDIO", "SWIFTFIND_SUPPRESS_STDIO")
.map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
.unwrap_or(false);
let suppress_for_background = options.command == RuntimeCommand::Run && options.background;
STDIO_LOGGING_ENABLED.store(
!(suppress_from_env || suppress_for_background),
Ordering::Relaxed,
);
}
fn should_log_to_stdio() -> bool {
STDIO_LOGGING_ENABLED.load(Ordering::Relaxed)
}
fn log_info(message: &str) {
if should_log_to_stdio() {
println!("{message}");
}
crate::logging::info(message);
}
fn log_warn(message: &str) {
if should_log_to_stdio() {
eprintln!("{message}");
}
crate::logging::warn(message);
}
#[cfg(test)]
mod tests {
use super::{
adaptive_indexed_seed_limit, build_status_diagnostics_json,
can_use_indexed_prefix_cache, candidate_limit_for_query, dedupe_overlay_results,
filter_suppressed_uninstall_results, launch_overlay_selection,
hotkey_registration_recovery_message, hotkey_registration_status_text,
maybe_expand_uninstall_quick_shortcut, next_selection_index, parse_cli_args,
parse_status_diagnostics_snapshot, parse_tasklist_pid_lines, result_limit_for_query,
queued_discovery_reindex_is_due,
search_overlay_results, search_overlay_results_with_session,
should_block_hotkey_for_foreground_window, should_hide_known_start_menu_doc_sample_entry,
should_skip_non_searchable_query, summarize_query_profiles,
track_uninstall_title_suppression,
uninstall_confirmation_results, uninstall_target_title_from_action_title,
ForegroundWindowSnapshot, IndexedPrefixCache, OverlaySearchSession, RuntimeCommand,
RuntimeOptions, ACTION_UNINSTALL_CANCEL_ID, ACTION_UNINSTALL_CONFIRM_ID,
INDEXED_PREFIX_CACHE_MAX_SEED_LIMIT, INDEXED_PREFIX_CACHE_MIN_SEED_LIMIT,
UNINSTALL_QUERY_RESULT_LIMIT,
};
use crate::action_registry::{ACTION_DIAGNOSTICS_BUNDLE_ID, ACTION_WEB_SEARCH_PREFIX};
use crate::config::{Config, SearchMode};
use crate::core_service::CoreService;
use crate::index_store::open_memory;
use crate::model::SearchItem;
use crate::plugin_sdk::PluginRegistry;
use crate::query_dsl::ParsedQuery;
use crate::search::SearchFilter;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
#[test]
fn overlay_search_returns_ranked_results() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos();
let path = std::env::temp_dir().join(format!("nex-overlay-search-{unique}.tmp"));
std::fs::write(&path, b"ok").expect("temp file should be created");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
service
.upsert_item(&SearchItem::new(
"item-1",
"app",
"Visual Studio Code",
path.to_string_lossy().as_ref(),
))
.expect("item should upsert");
let parsed = ParsedQuery::parse("code", true);
let cfg = Config::default();
let plugins = PluginRegistry::default();
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 20)
.expect("search should succeed");
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "item-1");
std::fs::remove_file(path).expect("temp file should be removed");
}
#[test]
fn overlay_launch_selection_launches_selected_item() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos();
let launch_path = std::env::temp_dir().join(format!("nex-launch-flow-{unique}.tmp"));
std::fs::write(&launch_path, b"ok").expect("temp launch file should be created");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
service
.upsert_item(&SearchItem::new(
"item-1",
"app",
"Code Launcher",
launch_path.to_string_lossy().as_ref(),
))
.expect("item should upsert");
let parsed = ParsedQuery::parse("code", true);
let cfg = Config::default();
let plugins = PluginRegistry::default();
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 20)
.expect("search should succeed");
launch_overlay_selection(&service, &cfg, &plugins, &results, 0, "launch target")
.expect("launch should succeed");
std::fs::remove_file(&launch_path).expect("temp launch file should be removed");
}
#[test]
fn overlay_launch_selection_reports_error_for_missing_path() {
let missing_path = std::env::temp_dir().join("nex-does-not-exist-launch-flow.exe");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
let item = SearchItem::new(
"missing",
"file",
"Missing Item",
missing_path.to_string_lossy().as_ref(),
);
service
.upsert_item(&SearchItem::new(
"missing",
"file",
"Missing Item",
missing_path.to_string_lossy().as_ref(),
))
.expect("item should upsert");
let results = vec![item];
let cfg = Config::default();
let plugins = PluginRegistry::default();
let error = launch_overlay_selection(&service, &cfg, &plugins, &results, 0, "missing")
.expect_err("launch should fail");
assert!(error.contains("launch failed:"));
}
#[test]
fn overlay_launch_selection_rejects_out_of_range_index() {
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
let results = vec![SearchItem::new("item-1", "app", "One", "C:\\One.exe")];
let cfg = Config::default();
let plugins = PluginRegistry::default();
let error = launch_overlay_selection(&service, &cfg, &plugins, &results, 1, "out")
.expect_err("selection should fail");
assert!(error.contains("selected index out of range"));
}
#[test]
fn overlay_launch_selection_rejects_empty_results() {
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
let cfg = Config::default();
let plugins = PluginRegistry::default();
let error = launch_overlay_selection(&service, &cfg, &plugins, &[], 0, "")
.expect_err("empty selection should fail");
assert_eq!(error, "no result selected");
}
#[test]
fn selection_index_bounds_are_stable() {
assert_eq!(next_selection_index(0, 0, 1), 0);
assert_eq!(next_selection_index(0, 3, -1), 0);
assert_eq!(next_selection_index(1, 3, -1), 0);
assert_eq!(next_selection_index(1, 3, 1), 2);
assert_eq!(next_selection_index(2, 3, 1), 2);
assert_eq!(next_selection_index(1, 3, 0), 1);
assert_eq!(next_selection_index(5, 3, 0), 2);
}
#[test]
fn candidate_limit_adapts_to_query_shape() {
let all = SearchFilter::default();
let empty_all = candidate_limit_for_query(20, &all, "", false);
let short_all = candidate_limit_for_query(20, &all, "v", false);
let medium_all = candidate_limit_for_query(20, &all, "vi", false);
let long_all = candidate_limit_for_query(20, &all, "vivaldi", false);
assert!(empty_all <= short_all);
assert!(short_all < medium_all);
assert!(medium_all <= long_all);
let actions = SearchFilter {
mode: SearchMode::Actions,
..SearchFilter::default()
};
let short_actions = candidate_limit_for_query(20, &actions, "v", true);
assert!(short_actions < long_all);
}
#[test]
fn uninstall_queries_use_expanded_result_limit() {
let parsed = ParsedQuery::parse(">uninstall", true);
let limit = result_limit_for_query(20, &parsed);
assert_eq!(limit, UNINSTALL_QUERY_RESULT_LIMIT);
let non_uninstall = ParsedQuery::parse(">web rust", true);
let non_limit = result_limit_for_query(20, &non_uninstall);
assert_eq!(non_limit, 20);
}
#[test]
fn quick_uninstall_shortcut_expands_only_on_initial_u() {
assert_eq!(
maybe_expand_uninstall_quick_shortcut(">u", ">"),
Some(">u ".to_string())
);
assert_eq!(maybe_expand_uninstall_quick_shortcut(">u", ">u"), None);
assert_eq!(
maybe_expand_uninstall_quick_shortcut(">u", ">u something"),
None
);
}
#[test]
fn uninstall_action_title_extracts_target_name() {
assert_eq!(
uninstall_target_title_from_action_title("Uninstall Discord"),
Some("Discord".to_string())
);
assert_eq!(
uninstall_target_title_from_action_title("uninstall Visual Studio Code "),
Some("Visual Studio Code".to_string())
);
assert_eq!(
uninstall_target_title_from_action_title("Open Discord"),
None
);
}
#[test]
fn uninstall_title_suppression_tracks_uniques() {
let mut suppressed = Vec::new();
track_uninstall_title_suppression(&mut suppressed, "Uninstall Discord");
track_uninstall_title_suppression(&mut suppressed, "uninstall discord");
track_uninstall_title_suppression(&mut suppressed, "Open Discord");
assert_eq!(suppressed, vec!["Discord".to_string()]);
}
#[test]
fn uninstall_confirmation_results_are_confirm_then_cancel() {
let uninstall_action = SearchItem::new(
"action:uninstall:discord",
"action",
"Uninstall Discord",
"shell:AppsFolder\\Discord",
);
let results = uninstall_confirmation_results(&uninstall_action);
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, ACTION_UNINSTALL_CONFIRM_ID);
assert_eq!(results[1].id, ACTION_UNINSTALL_CANCEL_ID);
assert!(results[0].title.contains("Discord"));
assert_eq!(results[1].title, "Cancel");
}
#[test]
fn suppressed_uninstall_results_are_filtered_from_results() {
let mut results = vec![
SearchItem::new("app-discord", "app", "Discord", "C:\\Discord\\Discord.exe"),
SearchItem::new(
"__nex_action_uninstall__:discord",
"action",
"Uninstall Discord",
"Vendor application",
),
SearchItem::new(
"app-vscode",
"app",
"Visual Studio Code",
"C:\\Code\\Code.exe",
),
SearchItem::new("file-readme", "file", "readme.md", "C:\\repo\\readme.md"),
];
let suppressed = vec!["Discord".to_string()];
filter_suppressed_uninstall_results(&mut results, &suppressed);
assert_eq!(results.len(), 2);
assert!(results.iter().all(|item| item.id != "app-discord"));
assert!(results
.iter()
.all(|item| item.id != "__nex_action_uninstall__:discord"));
assert!(results.iter().any(|item| item.id == "app-vscode"));
assert!(results.iter().any(|item| item.id == "file-readme"));
}
#[test]
fn hides_known_start_menu_doc_and_sample_entries() {
let docs = SearchItem::new(
"app-docs",
"app",
"Documentation Desktop Apps",
"shell:AppsFolder\\Contoso.DocumentationDesktopApps",
);
let sample = SearchItem::new(
"app-sample",
"app",
"Sample UWP Apps",
"shell:AppsFolder\\Contoso.SampleUwpApps",
);
let normal = SearchItem::new(
"app-normal",
"app",
"Discord",
"shell:AppsFolder\\Discord.Discord",
);
let non_shell = SearchItem::new(
"app-nonshell",
"app",
"Sample Tool",
"C:\\Tools\\SampleTool.exe",
);
let manual_lnk = SearchItem::new(
"app-manual",
"app",
"User Manual",
"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Tool\\User Manual.lnk",
);
let faq_pdf = SearchItem::new(
"app-faq",
"app",
"Tool FAQ",
"shell:AppsFolder\\Vendor.ToolFAQ.pdf",
);
let normal_lnk = SearchItem::new(
"app-normal-lnk",
"app",
"Discord",
"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Discord\\Discord.lnk",
);
assert!(should_hide_known_start_menu_doc_sample_entry(&docs));
assert!(should_hide_known_start_menu_doc_sample_entry(&sample));
assert!(should_hide_known_start_menu_doc_sample_entry(&manual_lnk));
assert!(should_hide_known_start_menu_doc_sample_entry(&faq_pdf));
assert!(!should_hide_known_start_menu_doc_sample_entry(&normal));
assert!(!should_hide_known_start_menu_doc_sample_entry(&non_shell));
assert!(!should_hide_known_start_menu_doc_sample_entry(&normal_lnk));
}
#[test]
fn prefix_cache_predicate_requires_same_filter_and_extended_query() {
let cache = IndexedPrefixCache {
normalized_query: "vi".to_string(),
indexed_filter: SearchFilter::default(),
seed_items: vec![SearchItem::new(
"app-1",
"app",
"Vivaldi",
"C:\\Vivaldi.exe",
)],
};
assert!(can_use_indexed_prefix_cache(
&cache,
true,
"viv",
&SearchFilter::default()
));
assert!(!can_use_indexed_prefix_cache(
&cache,
true,
"vi",
&SearchFilter::default()
));
assert!(!can_use_indexed_prefix_cache(
&cache,
true,
"xvi",
&SearchFilter::default()
));
let different_mode = SearchFilter {
mode: SearchMode::Apps,
..SearchFilter::default()
};
assert!(!can_use_indexed_prefix_cache(
&cache,
true,
"viv",
&different_mode
));
assert!(!can_use_indexed_prefix_cache(
&cache,
false,
"viv",
&SearchFilter::default()
));
}
#[test]
fn game_mode_does_not_block_standard_maximized_apps() {
let snapshot = ForegroundWindowSnapshot {
class_name: "Chrome_WidgetWin_1".to_string(),
process_name: "chrome.exe".to_string(),
process_path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe".to_string(),
covers_monitor: true,
has_standard_frame: true,
maximized: true,
};
assert!(!should_block_hotkey_for_foreground_window(&snapshot));
}
#[test]
fn game_mode_blocks_known_game_like_borderless_windows() {
let snapshot = ForegroundWindowSnapshot {
class_name: "UnrealWindow".to_string(),
process_name: "VALORANT-Win64-Shipping.exe".to_string(),
process_path: "C:\\Riot Games\\VALORANT\\live\\ShooterGame\\Binaries\\Win64\\VALORANT-Win64-Shipping.exe".to_string(),
covers_monitor: true,
has_standard_frame: false,
maximized: false,
};
assert!(should_block_hotkey_for_foreground_window(&snapshot));
}
#[test]
fn repeated_overlay_query_uses_final_cache() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos();
let path = std::env::temp_dir().join(format!("nex-overlay-cache-{unique}.tmp"));
std::fs::write(&path, b"ok").expect("temp file should be created");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
service
.upsert_item(&SearchItem::new(
"item-1",
"app",
"Vivaldi",
path.to_string_lossy().as_ref(),
))
.expect("item should upsert");
let cfg = Config::default();
let plugins = PluginRegistry::default();
let parsed = ParsedQuery::parse("vi", true);
let mut session = OverlaySearchSession::default();
let first = search_overlay_results_with_session(
&service,
&cfg,
&plugins,
&parsed,
20,
&mut session,
)
.expect("first query should succeed");
let sample_count_after_first = session.indexed_latency_ms.len();
let second = search_overlay_results_with_session(
&service,
&cfg,
&plugins,
&parsed,
20,
&mut session,
)
.expect("second query should succeed");
assert_eq!(first, second);
assert_eq!(session.indexed_latency_ms.len(), sample_count_after_first);
assert!(!session.final_query_cache.is_empty());
std::fs::remove_file(path).expect("temp file should be removed");
}
#[test]
fn adaptive_seed_limit_reduces_on_high_latency_window() {
let mut session = OverlaySearchSession::default();
session
.indexed_latency_ms
.extend(std::iter::repeat(170_u128).take(12));
let base = 320;
let tuned = adaptive_indexed_seed_limit(&session, 120, 1, base);
assert!(tuned < base);
assert!(tuned >= INDEXED_PREFIX_CACHE_MIN_SEED_LIMIT / 2);
assert!(tuned <= INDEXED_PREFIX_CACHE_MAX_SEED_LIMIT);
}
#[test]
fn parses_background_run_args() {
let args = vec!["--background".to_string()];
let options = parse_cli_args(&args).expect("args should parse");
assert_eq!(
options,
RuntimeOptions {
command: RuntimeCommand::Run,
background: true,
}
);
}
#[test]
fn parses_lifecycle_commands() {
let args = vec!["--status".to_string()];
let options = parse_cli_args(&args).expect("status should parse");
assert_eq!(options.command, RuntimeCommand::Status);
assert!(!options.background);
let args = vec!["--status-json".to_string()];
let options = parse_cli_args(&args).expect("status-json should parse");
assert_eq!(options.command, RuntimeCommand::StatusJson);
assert!(!options.background);
}
#[test]
fn parses_diagnostics_bundle_command() {
let args = vec!["--diagnostics-bundle".to_string()];
let options = parse_cli_args(&args).expect("diagnostics command should parse");
assert_eq!(options.command, RuntimeCommand::DiagnosticsBundle);
assert!(!options.background);
}
#[test]
fn parses_set_launch_at_startup_command() {
let args = vec!["--set-launch-at-startup=true".to_string()];
let options = parse_cli_args(&args).expect("startup command should parse");
assert_eq!(options.command, RuntimeCommand::SetLaunchAtStartup(true));
assert!(!options.background);
let args = vec!["--set-launch-at-startup=false".to_string()];
let options = parse_cli_args(&args).expect("startup command should parse");
assert_eq!(options.command, RuntimeCommand::SetLaunchAtStartup(false));
assert!(!options.background);
}
#[test]
fn rejects_invalid_set_launch_at_startup_value() {
let args = vec!["--set-launch-at-startup=maybe".to_string()];
let error = parse_cli_args(&args).expect_err("invalid value should fail");
assert!(error.contains("invalid value for --set-launch-at-startup"));
}
#[test]
fn rejects_background_with_non_run_commands() {
let args = vec!["--quit".to_string(), "--background".to_string()];
let error = parse_cli_args(&args).expect_err("invalid combination should fail");
assert!(error.contains("background mode"));
}
#[test]
fn command_mode_returns_action_results() {
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
let cfg = Config::default();
let plugins = PluginRegistry::default();
let parsed = ParsedQuery::parse(">diag", true);
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 10)
.expect("search should succeed");
assert!(results
.iter()
.any(|item| item.id == ACTION_DIAGNOSTICS_BUNDLE_ID));
}
#[test]
fn command_mode_includes_web_search_action() {
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
let cfg = Config::default();
let plugins = PluginRegistry::default();
let parsed = ParsedQuery::parse(">nex roadmap", true);
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 10)
.expect("search should succeed");
assert!(results
.iter()
.any(|item| item.id.starts_with(ACTION_WEB_SEARCH_PREFIX)));
}
#[test]
fn short_single_letter_query_in_all_mode_biases_to_apps() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos();
let app_path = std::env::temp_dir().join(format!("nex-short-query-app-{unique}.tmp"));
let file_path =
std::env::temp_dir().join(format!("nex-short-query-file-{unique}.tmp"));
std::fs::write(&app_path, b"ok").expect("app temp file should be created");
std::fs::write(&file_path, b"ok").expect("file temp file should be created");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
service
.upsert_item(&SearchItem::new(
"app-1",
"app",
"Vivaldi Browser",
app_path.to_string_lossy().as_ref(),
))
.expect("app should upsert");
service
.upsert_item(&SearchItem::new(
"file-1",
"file",
"Vacation Notes",
file_path.to_string_lossy().as_ref(),
))
.expect("file should upsert");
let parsed = ParsedQuery::parse("v", true);
let cfg = Config::default();
let plugins = PluginRegistry::default();
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 20)
.expect("search should succeed");
assert!(results.iter().any(|item| item.id == "app-1"));
assert!(!results.iter().any(|item| item.id == "file-1"));
std::fs::remove_file(app_path).expect("app temp file should be removed");
std::fs::remove_file(file_path).expect("file temp file should be removed");
}
#[test]
fn short_two_letter_query_in_all_mode_biases_to_apps() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be valid")
.as_nanos();
let app_path = std::env::temp_dir().join(format!("nex-short-two-app-{unique}.tmp"));
let file_path = std::env::temp_dir().join(format!("nex-short-two-file-{unique}.tmp"));
std::fs::write(&app_path, b"ok").expect("app temp file should be created");
std::fs::write(&file_path, b"ok").expect("file temp file should be created");
let service = CoreService::with_connection(Config::default(), open_memory().unwrap())
.expect("service should initialize");
service
.upsert_item(&SearchItem::new(
"app-1",
"app",
"Valorant",
app_path.to_string_lossy().as_ref(),
))
.expect("app should upsert");
service
.upsert_item(&SearchItem::new(
"file-1",
"file",
"Valuation Notes",
file_path.to_string_lossy().as_ref(),
))
.expect("file should upsert");
let parsed = ParsedQuery::parse("va", true);
let cfg = Config::default();
let plugins = PluginRegistry::default();
let results = search_overlay_results(&service, &cfg, &plugins, &parsed, 20)
.expect("search should succeed");
assert!(results.iter().any(|item| item.id == "app-1"));
assert!(!results.iter().any(|item| item.id == "file-1"));
std::fs::remove_file(app_path).expect("app temp file should be removed");
std::fs::remove_file(file_path).expect("file temp file should be removed");
}
#[test]
fn dedupes_duplicate_app_titles_for_overlay() {
let mut results = vec![
SearchItem::new("a1", "app", "Steam", "C:\\One\\Steam.lnk"),
SearchItem::new("a2", "app", "Steam", "C:\\Two\\Steam.lnk"),
SearchItem::new("a3", "app", "Calculator", "C:\\Calc.lnk"),
];
dedupe_overlay_results(&mut results);
assert_eq!(results.len(), 2);
assert_eq!(results[0].title, "Steam");
assert_eq!(results[1].title, "Calculator");
}
#[test]
fn dedupes_non_app_entries_by_normalized_path() {
let mut results = vec![
SearchItem::new("f1", "file", "Doc A", "C:/Users/Admin/Docs/test.txt"),
SearchItem::new("f2", "file", "Doc B", "C:\\Users\\Admin\\Docs\\test.txt"),
SearchItem::new("f3", "file", "Doc C", "C:\\Users\\Admin\\Docs\\other.txt"),
];
dedupe_overlay_results(&mut results);
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, "f1");
assert_eq!(results[1].id, "f3");
}
#[test]
fn dedupes_lnk_file_when_matching_app_title_exists() {
let mut results = vec![
SearchItem::new("a1", "app", "Framer", "C:\\ProgramData\\Framer.lnk"),
SearchItem::new(
"f1",
"file",
"Framer.lnk",
"C:\\Users\\Admin\\Desktop\\Framer.lnk",
),
SearchItem::new(
"f2",
"file",
"Framer Notes.lnk",
"C:\\Users\\Admin\\Desktop\\Framer Notes.lnk",
),
];
dedupe_overlay_results(&mut results);
let ids: Vec<&str> = results.iter().map(|item| item.id.as_str()).collect();
assert_eq!(ids, vec!["a1", "f2"]);
}
#[test]
fn parses_status_diagnostics_snapshot_from_log_content() {
let content = "\
[0] [INFO] [nex] startup_phase phase=overlay_ready elapsed_ms=41
[0] [INFO] [nex] startup_phase phase=hotkey_ready elapsed_ms=56 hotkey=Ctrl+Space
[0] [WARN] [nex] hotkey_registration_issue hotkey=Ctrl+Space suggestions=Ctrl+Shift+Space|Alt+Space error=conflict
[0] [INFO] [nex] startup_phase phase=indexing_started elapsed_ms=7 initial_cache_empty=true cached_items=0
[0] [INFO] [nex] startup_phase phase=indexing_completed elapsed_ms=2815 worker_elapsed_ms=2809 indexed_items=310 discovered=320 upserted=16 removed=4
[0] [INFO] [nex] startup_phase phase=cache_applied elapsed_ms=2820 cached_items=310 initial_cache_empty=true
[1] [INFO] [nex] startup indexed_items=310 discovered=320 upserted=16 removed=4
[2] [INFO] [nex] index_provider name=start-menu-apps discovered=120 upserted=4 removed=1 elapsed_ms=42
[3] [INFO] [nex] provider_freshness name=filesystem skipped=false last_scan_age_secs=0 reconcile_interval_secs=1800 has_stamp=true
[4] [INFO] [nex] stale_prune scanned=512 removed=3 cached_items_remaining=738
[5] [INFO] [nex] cache_compaction input_total=812 retained=596 dropped=216 retained_apps=20 retained_file_folders=576 retained_other=0 effective_file_seed_cap=576 broad_root_mode=true active_memory_target_mb=72
[6] [INFO] [nex] overlay_icon_cache reason=cache_clear hits=12 misses=8 load_failures=1 evictions=0 cleared_entries=9 live_entries=0 max_entries=90
";
let snapshot = parse_status_diagnostics_snapshot(content).expect("snapshot should parse");
assert!(snapshot
.overlay_ready_line
.as_deref()
.unwrap_or_default()
.contains("phase=overlay_ready"));
assert!(snapshot
.hotkey_ready_line
.as_deref()
.unwrap_or_default()
.contains("phase=hotkey_ready"));
assert!(snapshot
.hotkey_registration_issue_line
.as_deref()
.unwrap_or_default()
.contains("hotkey_registration_issue hotkey=Ctrl+Space"));
assert!(snapshot
.indexing_started_line
.as_deref()
.unwrap_or_default()
.contains("phase=indexing_started"));
assert!(snapshot
.indexing_completed_line
.as_deref()
.unwrap_or_default()
.contains("phase=indexing_completed"));
assert!(snapshot
.cache_applied_line
.as_deref()
.unwrap_or_default()
.contains("phase=cache_applied"));
assert!(snapshot
.startup_index_line
.as_deref()
.unwrap_or_default()
.contains("startup indexed_items=310"));
assert!(snapshot
.last_provider_line
.as_deref()
.unwrap_or_default()
.contains("index_provider name=start-menu-apps"));
assert!(snapshot
.last_provider_freshness_line
.as_deref()
.unwrap_or_default()
.contains("provider_freshness name=filesystem"));
assert!(snapshot
.last_stale_prune_line
.as_deref()
.unwrap_or_default()
.contains("stale_prune scanned=512"));
assert!(snapshot
.last_cache_compaction_line
.as_deref()
.unwrap_or_default()
.contains("cache_compaction input_total=812"));
assert!(snapshot
.last_icon_cache_line
.as_deref()
.unwrap_or_default()
.contains("overlay_icon_cache reason=cache_clear"));
}
#[test]
fn status_diagnostics_json_includes_startup_lifecycle_tokens() {
let content = "\
[1773000001] [INFO] [nex] startup_phase phase=overlay_ready elapsed_ms=33
[1773000002] [INFO] [nex] startup_phase phase=hotkey_ready elapsed_ms=48 hotkey=Ctrl+Space
[1773000002] [WARN] [nex] hotkey_registration_issue hotkey=Ctrl+Space suggestions=Ctrl+Shift+Space|Alt+Space error=conflict
[1773000003] [INFO] [nex] startup_phase phase=indexing_started elapsed_ms=6 initial_cache_empty=true cached_items=0
[1773000028] [INFO] [nex] startup_phase phase=indexing_completed elapsed_ms=2600 worker_elapsed_ms=2593 indexed_items=310 discovered=320 upserted=16 removed=4
[1773000029] [INFO] [nex] startup_phase phase=cache_applied elapsed_ms=2605 cached_items=310 initial_cache_empty=true
[1773000030] [INFO] [nex] provider_freshness name=filesystem skipped=false last_scan_age_secs=0 reconcile_interval_secs=1800 has_stamp=true
[1773000031] [INFO] [nex] stale_prune scanned=512 removed=3 cached_items_remaining=738
[1773000032] [INFO] [nex] cache_compaction input_total=812 retained=596 dropped=216 retained_apps=20 retained_file_folders=576 retained_other=0 effective_file_seed_cap=576 broad_root_mode=true active_memory_target_mb=72
[1773000033] [INFO] [nex] overlay_icon_cache reason=cache_clear hits=12 misses=8 load_failures=1 evictions=0 cleared_entries=9 live_entries=0 max_entries=90
";
let snapshot = parse_status_diagnostics_snapshot(content).expect("snapshot should parse");
let json = build_status_diagnostics_json(&snapshot);
assert_eq!(
json["startup_lifecycle"]["overlay_ready"]["tokens"]["elapsed_ms"],
serde_json::json!(33)
);
assert_eq!(
json["startup_lifecycle"]["hotkey_ready"]["tokens"]["hotkey"],
serde_json::json!("Ctrl+Space")
);
assert_eq!(
json["hotkey_issue"]["tokens"]["suggestions"],
serde_json::json!("Ctrl+Shift+Space|Alt+Space")
);
assert_eq!(
json["hotkey_issue"]["epoch_secs"],
serde_json::json!(1773000002_u64)
);
assert_eq!(
json["startup_lifecycle"]["indexing_started"]["tokens"]["initial_cache_empty"],
serde_json::json!(true)
);
assert_eq!(
json["startup_lifecycle"]["indexing_completed"]["tokens"]["worker_elapsed_ms"],
serde_json::json!(2593)
);
assert_eq!(
json["startup_lifecycle"]["cache_applied"]["tokens"]["cached_items"],
serde_json::json!(310)
);
assert_eq!(
json["startup_lifecycle"]["cache_applied"]["epoch_secs"],
serde_json::json!(1773000029_u64)
);
assert_eq!(
json["provider_freshness"]["reconcile_interval_secs"],
serde_json::json!(1800)
);
assert_eq!(
json["stale_prune"]["removed"],
serde_json::json!(3)
);
assert_eq!(
json["cache_compaction"]["effective_file_seed_cap"],
serde_json::json!(576)
);
assert_eq!(
json["cache_compaction"]["broad_root_mode"],
serde_json::json!(true)
);
assert_eq!(
json["icon_cache"]["max_entries"],
serde_json::json!(90)
);
}
#[test]
fn queued_reindex_starts_only_after_due_time() {
let now = Instant::now();
assert!(!queued_discovery_reindex_is_due(
true,
true,
Some(now + std::time::Duration::from_millis(5)),
now
));
assert!(queued_discovery_reindex_is_due(
true,
true,
Some(now),
now
));
assert!(!queued_discovery_reindex_is_due(true, false, Some(now), now));
assert!(!queued_discovery_reindex_is_due(false, true, Some(now), now));
assert!(!queued_discovery_reindex_is_due(true, true, None, now));
}
#[test]
fn returns_none_for_status_snapshot_without_diagnostics_tokens() {
let content = "[1] [INFO] [nex] status: running\n";
assert!(parse_status_diagnostics_snapshot(content).is_none());
}
#[test]
fn hotkey_registration_messages_include_recovery_guidance() {
let message = hotkey_registration_recovery_message(
"Ctrl+Space",
std::path::Path::new("C:\\Users\\Admin\\AppData\\Roaming\\Nex\\config.toml"),
);
assert!(message.contains("Hotkey 'Ctrl+Space' is unavailable."));
assert!(message.contains("Ctrl+Shift+Space"));
assert!(message.contains("config.toml"));
let status = hotkey_registration_status_text("Ctrl+Space");
assert!(status.contains("Hotkey unavailable: Ctrl+Space."));
assert!(status.contains("Ctrl+Shift+Space"));
}
#[test]
fn summarizes_query_profiles_from_log_content() {
let content = "\
[1] [INFO] [nex] query_profile q=\"v\" mode=all candidate_limit=60 indexed_seed_limit=240 short_app_bias=true indexed_cache_hit=false indexed_count=20 indexed_ms=20 provider_count=0 provider_ms=0 action_count=0 action_ms=0 built_in_actions=0 plugin_actions=0 clipboard_count=0 clipboard_ms=0 rank_ms=0 total_ms=21
[2] [INFO] [nex] query_profile q=\"va\" mode=all candidate_limit=80 indexed_seed_limit=160 short_app_bias=true indexed_cache_hit=false indexed_count=20 indexed_ms=26 provider_count=0 provider_ms=0 action_count=0 action_ms=0 built_in_actions=0 plugin_actions=0 clipboard_count=0 clipboard_ms=0 rank_ms=0 total_ms=27
[3] [INFO] [nex] query_profile q=\"vala\" mode=all candidate_limit=120 indexed_seed_limit=240 short_app_bias=false indexed_cache_hit=false indexed_count=20 indexed_ms=54 provider_count=0 provider_ms=0 action_count=0 action_ms=0 built_in_actions=0 plugin_actions=0 clipboard_count=0 clipboard_ms=0 rank_ms=0 total_ms=55
";
let summary = summarize_query_profiles(content).expect("summary should parse");
assert_eq!(summary.samples, 3);
assert_eq!(summary.p95_total_ms, 55);
assert_eq!(summary.short_query_samples, 2);
assert_eq!(summary.short_query_app_bias_rate_pct, 100);
assert_eq!(summary.short_query_p95_total_ms, 27);
}
#[test]
fn skips_non_searchable_symbol_only_query() {
let parsed = ParsedQuery::parse("-", true);
let normalized = crate::model::normalize_for_search(parsed.free_text.trim());
assert!(should_skip_non_searchable_query(&parsed, &normalized));
let parsed_command = ParsedQuery::parse(">-", true);
let normalized_command =
crate::model::normalize_for_search(parsed_command.free_text.trim());
assert!(!should_skip_non_searchable_query(
&parsed_command,
&normalized_command
));
}
#[test]
fn parses_tasklist_pid_lines_from_list_output() {
let content = "\
Image Name: nex.exe
PID: 1124
Session Name: Console
Image Name: nex.exe
PID: 2208
Session Name: Console
";
let pids = parse_tasklist_pid_lines(content);
assert_eq!(pids, vec![1124, 2208]);
}
}