mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
On macOS, OpenIDEA is now listed under Finder's Open With for text and source files and for folders. It never becomes the default app for any of them: all document types are declared with rank Alternate, and only in tauri.macos.conf.json, so Windows and Linux installers register nothing. Files opened from Finder, the Dock or a second instance share one routing path (plan_open). A file inside a local project that is already open, or still opening, goes to that project's window, choosing the deepest root when projects are nested. Any other file goes to the existing single-file window, or to a new one. A folder focuses the window that already has it open, otherwise it opens as a project. Paths that arrive before setup are queued and opened with the first launch. The single-file window now closes with its last tab, restores its files the next time it opens, and only offers Open File and drop hints when empty. Also fixes a regression: reopening projects on startup no longer replaces a file opened at cold start. Refs #128
182 lines
7.6 KiB
Rust
182 lines
7.6 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 remote_dev;
|
|
|
|
mod state;
|
|
mod theme_frame;
|
|
mod windows;
|
|
|
|
use parking_lot::Mutex;
|
|
use state::AppState;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
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 = ide_host::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)));
|
|
ide_sdk::java::add_jdk_root(&ide_tools::jdk::jdks_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());
|
|
|
|
// Paths macOS asked to open (Finder's Open With, the Dock) before setup ran: the first window takes them.
|
|
let early_opens: Arc<Mutex<Option<Vec<PathBuf>>>> = Arc::default();
|
|
let setup_opens = early_opens.clone();
|
|
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::path_launch(path));
|
|
}
|
|
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);
|
|
if let Some(paths) = setup_opens.lock().take() {
|
|
windows::open_paths_at_launch(app.handle(), paths);
|
|
}
|
|
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(move |app, event| {
|
|
#[cfg(target_os = "macos")]
|
|
if let tauri::RunEvent::Opened { urls } = &event {
|
|
let paths = windows::opened_paths(urls);
|
|
if app.try_state::<WindowsState>().is_some() {
|
|
windows::open_paths(app, paths);
|
|
} else {
|
|
early_opens.lock().get_or_insert_default().extend(paths);
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = &early_opens;
|
|
// 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" | "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)),
|
|
_ => {}
|
|
},
|
|
}
|
|
}
|