//! OpenIDEA desktop shell: wires the backend crates into Tauri. //! //! This crate is glue only. Each `commands/.rs` is a thin, typed //! adapter from IPC to a backend crate; no domain logic lives here. mod commands; mod events; mod logging; mod menu; mod shell_env; mod state; mod windows; use state::AppState; use tauri::{Emitter, Manager}; use windows::WindowsState; /// Builds the tauri-specta builder holding every command and exported type. /// Also used by `export_bindings` (tests) to regenerate `src/bindings.ts`. pub fn specta_builder() -> tauri_specta::Builder { commands::builder().typ::().dangerously_cast_bigints_to_number() } pub fn export_bindings(builder: &tauri_specta::Builder) { builder .export( specta_typescript::Typescript::default().header("// @ts-nocheck\n/* eslint-disable */\n// Generated by tauri-specta. Do not edit; run `pnpm gen:bindings`."), concat!(env!("CARGO_MANIFEST_DIR"), "/../src/bindings.ts"), ) .expect("failed to export TypeScript bindings"); } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { ide_workspace::process::mark_start(); let context = tauri::generate_context!(); logging::init(&context.config().identifier); let refresh_shell_path = shell_env::inherit_login_shell_path(&context.config().identifier); // Managed LSP/DAP installs; must run before other threads start and before PATH is cached. if let Some(data) = dirs::data_dir() { ide_tools::extend_process_path(&data.join(&context.config().identifier)); ide_dap::adapter::set_tools_root(ide_tools::tools_dir(&data.join(&context.config().identifier))); } if let Some(refresh) = refresh_shell_path { refresh(); } let builder = specta_builder(); #[cfg(debug_assertions)] export_bindings(&builder); let app = tauri::Builder::default(); // Single instance first: a second launch focuses this one and opens its path argument. // Release only, so a dev build can run beside an installed release (same identifier). #[cfg(not(debug_assertions))] let app = app.plugin(tauri_plugin_single_instance::init(windows::on_second_instance)); let app = app .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin( tauri_plugin_window_state::Builder::default() .with_state_flags(window_state_flags()) // One remembered geometry shared by every project's settings window. .map_label(|l| if windows::is_settings_window(l) { "settings" } else { l }) .build(), ) // Inert until an endpoint is configured (see `commands::window::app_check_update`). .plugin(tauri_plugin_updater::Builder::new().build()); // E2E automation bridge (Tauri MCP), debug builds only, localhost only. #[cfg(debug_assertions)] let app = app.plugin(tauri_plugin_mcp_bridge::Builder::new().bind_address("127.0.0.1").build()); app.invoke_handler(builder.invoke_handler()) .setup(move |app| { builder.mount_events(app); let state = AppState::new(app.handle().clone())?; let windows = WindowsState::default(); if let Ok(cwd) = std::env::current_dir() && let Some(path) = windows::cli_path(&std::env::args().collect::>(), &cwd) { windows.set_launch("main", windows::Launch { project: Some(path.into()), ..Default::default() }); } let menu_state = menu::MenuState::default(); menu::install(app.handle(), &menu_state, &state.recent.list()); app.manage(state); app.manage(windows); app.manage(menu_state); app.on_menu_event(on_menu_event); windows::create_main_window(app.handle())?; Ok(()) }) .on_window_event(|window, event| match event { tauri::WindowEvent::Focused(true) => { if let Some(w) = window.try_state::() { w.focused(window.label()); } } tauri::WindowEvent::Destroyed => { if let Some(settings) = window.app_handle().get_webview_window(&windows::settings_label(window.label())) { let _ = settings.destroy(); } if let Some(state) = window.try_state::() { state.close_project(window.label()); } if let Some(w) = window.try_state::() { w.forget(window.label()); } } _ => {} }) .build(context) .expect("error while running OpenIDEA") .run(|app, event| { // Quit after installing an update (`app_install_update`): relaunch. if let tauri::RunEvent::Exit = event && app.try_state::().is_some_and(|w| w.restart_requested()) { tauri::process::restart(&app.env()); } }); } fn window_state_flags() -> tauri_plugin_window_state::StateFlags { use tauri_plugin_window_state::StateFlags; StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN } /// Routes IDE menu items to the focused window's frontend (`executeAction`). fn on_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) { let id = event.id().as_ref(); let Some(route) = menu::route(&menu::spec(menu::Os::current()), id) else { return }; match windows::target_window(app) { Some(w) => { // Settings is modal for its project window: only app-level items get through, to the parent. let label = match windows::settings_parent(w.label()) { Some(parent) if matches!(route.action.as_str(), "app.quit" | "file.settings" | "file.newProjectsSettings") => { parent } Some(_) => return, None => w.label(), }; if let Err(e) = app.emit_to(label, menu::MENU_EVENT, &route) { tracing::warn!("failed to deliver menu event {id}: {e}"); } } // No window left (macOS keeps the menu bar): handle the few that make sense. None => match route.action.as_str() { "app.quit" => app.exit(0), "window.new" | "file.openProject" => windows::spawn_window(app, None), menu::OPEN_RECENT_ACTION => windows::spawn_window(app, route.arg.map(Into::into)), _ => {} }, } }