Files
openidea/src-tauri/src/lib.rs
T
l0ng-ai bf06ca7832 feat(app): Show a themed splash until the launch project opens
Project windows are created in setup with the saved theme's frame colour, and the splash covers the first render while the launch project opens, so a window no longer flashes white or the Welcome screen. Single-instance is now release-only so a dev build can run beside an installed release.
2026-10-23 15:40:41 +08:00

155 lines
6.5 KiB
Rust

//! OpenIDEA desktop shell: wires the backend crates into Tauri.
//!
//! This crate is glue only. Each `commands/<domain>.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<tauri::Wry> {
commands::builder().typ::<ide_core::IdeEvent>().dangerously_cast_bigints_to_number()
}
pub fn export_bindings(builder: &tauri_specta::Builder<tauri::Wry>) {
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::<Vec<_>>(), &cwd)
{
windows.set_launch("main", windows::Launch { project: Some(path.into()), settings_page: None });
}
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::<WindowsState>() {
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::<AppState>() {
state.close_project(window.label());
}
if let Some(w) = window.try_state::<WindowsState>() {
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::<WindowsState>().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") => 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)),
_ => {}
},
}
}