Файли
Walter WangтаClaude 3b608355a3 feat(themes): Built-in Solarized and One light/dark themes (#85)
Adds Solarized Light, Solarized Dark, One Light and One Dark as built-in themes across the workbench, editor, terminal and Mermaid, keeping the original palettes (MIT notices in THIRD_PARTY_LICENSES.md, mapping in docs/themes.md).

- Native window colours are read from tokens.css at build time (`theme_frame.rs`) instead of a second colour table.
- Startup restores the built-in palette before first paint.
- Editor schemes, the terminal and Mermaid refresh on switches between themes of the same kind.
- The plugin packages of the same themes are removed.
- A test requires every built-in palette theme to define each token (only listed ones may inherit from the base theme), so new tokens can't silently miss them.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 18:53:12 +08:00

161 рядки
6.7 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 theme_frame;
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)));
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());
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()), ..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::<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" | "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)),
_ => {}
},
}
}