|
| 1 | +// --------------------------------------------------------------------------------------------- |
| 2 | +// Mountain Command Handlers (handlers/commands.rs) |
| 3 | +// -------------------------------------------------------------------------------------------- |
| 4 | +// Implements the core logic for managing and executing commands within |
| 5 | +// Mountain, handling interactions originating from both the frontend (via |
| 6 | +// Track) and sidecars (via RPC/Vine/Track). It maintains the central command |
| 7 | +// registry. |
| 8 | +// |
| 9 | +// Responsibilities: |
| 10 | +// - Managing the command registry (stored in `AppState`) which tracks both |
| 11 | +// native Mountain commands and commands registered by sidecars (like Cocoon). |
| 12 | +// - Handling `$registerCommand` RPC calls from Cocoon: Registers a *proxy* |
| 13 | +// handler that forwards execution back to the originating sidecar via Vine. |
| 14 | +// - Handling `$unregisterCommand` RPC calls from Cocoon: Removes the |
| 15 | +// corresponding proxy handler if the requesting sidecar is the owner. |
| 16 | +// - Handling `$getCommands` RPC calls from Cocoon: Returns a combined list of |
| 17 | +// all registered native and proxied command IDs. |
| 18 | +// - Handling command execution requests (`handle_execute_command`, called by |
| 19 | +// Track): |
| 20 | +// - Looks up the command ID in the registry. |
| 21 | +// - If native: Executes the corresponding native logic (calling the stored |
| 22 | +// handler function). |
| 23 | +// - If proxied: Sends a request (`commands_executeContributedCommand`) via |
| 24 | +// Vine to the owning sidecar to execute the command there. |
| 25 | +// - Returns the result or error from the execution. |
| 26 | +// - Providing functions to register native commands during Mountain startup |
| 27 | +// (`register_native_command_internal`). |
| 28 | +// - Implementing the actual logic for native commands (e.g., |
| 29 | + |
| 30 | +// `handle_native_save_all`). |
| 31 | +// |
| 32 | +// Key Interactions: |
| 33 | +// - Interacts heavily with `AppState` to access/modify the `command_registry`. |
| 34 | +// - Called by `track::dispatch_sidecar_request` for RPC methods. |
| 35 | +// - Called by `track::dispatch_command` (indirectly via effect) for execution. |
| 36 | +// - Uses `vine::send_request` to proxy command execution back to sidecars. |
| 37 | +// - Executes native command logic directly or via `ActionEffect`s using |
| 38 | +// `AppRuntime`. |
| 39 | +// -------------------------------------------------------------------------------------------- |
| 40 | + |
| 41 | +use std::{ |
| 42 | + collections::HashMap, |
| 43 | + |
| 44 | + future::Future, |
| 45 | + |
| 46 | + pin::Pin, |
| 47 | + |
| 48 | + // Use standard Mutex |
| 49 | + sync::{Arc, Mutex as StdMutex, MutexGuard}, |
| 50 | +}; |
| 51 | + |
| 52 | +// Assume Land_Common provides necessary effects if used by native handlers |
| 53 | +use Land_Common::{command_effects, ui_effects, workspace_effects}; |
| 54 | +// Use log crate for logging |
| 55 | +use log; |
| 56 | +use serde_json::{Value, json}; |
| 57 | +use tauri::{AppHandle, Manager, Runtime as TauriRuntime, State, Window}; |
| 58 | + |
| 59 | +use crate::{ |
| 60 | + // Import AppState and CommandHandler enum |
| 61 | + app_state::{AppState, CommandHandler}, |
| 62 | + |
| 63 | + runtime::AppRuntime, |
| 64 | + |
| 65 | + // Track might not be needed directly if only called by it |
| 66 | + track, |
| 67 | + |
| 68 | + // Vine required for proxying back to sidecars |
| 69 | + vine, |
| 70 | +}; |
| 71 | + |
| 72 | +// --- Helper Functions --- |
| 73 | + |
| 74 | +/// Helper to create a structured error JSON string for handler results. |
| 75 | +fn create_handler_error_string(message:String, code:Option<&str>) -> String { |
| 76 | + json!({ "message": message, "code": code.unwrap_or("EUNKNOWN") }).to_string() |
| 77 | +} |
| 78 | + |
| 79 | +/// Helper to map Mutex lock poisoning errors to the handler's error string |
| 80 | +/// format. |
| 81 | +fn map_lock_error<T>(e:std::sync::PoisonError<MutexGuard<'_, T>>) -> String { |
| 82 | + create_handler_error_string(format!("Failed to acquire lock on command registry: {}", e), Some("ELOCKED")) |
| 83 | +} |
| 84 | + |
| 85 | +// --- Request Handlers (Called by Track dispatcher for sidecar requests) --- |
| 86 | + |
| 87 | +/// Handles the `commands_registerCommand` request from a sidecar shim. |
| 88 | +/// Stores the association between the sidecar and the registered command ID. |
| 89 | +pub async fn handle_register_command<R:TauriRuntime>( |
| 90 | + app:AppHandle<R>, |
| 91 | + |
| 92 | + sidecar_id:String, |
| 93 | + |
| 94 | + params:Value, |
| 95 | +) -> Result<Value, String> { |
| 96 | + let command_id = params |
| 97 | + .get("id") |
| 98 | + .and_then(|v| v.as_str()) |
| 99 | + .ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))? |
| 100 | + .to_string(); |
| 101 | + |
| 102 | + // Keep: Essential registration action log |
| 103 | + log::info!("[Cmd Handler] Registering PROXY for '{}' from '{}'", command_id, sidecar_id); |
| 104 | + |
| 105 | + let app_state = app.state::<AppState>(); |
| 106 | + |
| 107 | + let mut registry = app_state.command_registry.lock().map_err(map_lock_error)?; |
| 108 | + |
| 109 | + // Keep: Warning for overwrite is useful |
| 110 | + if registry.contains_key(&command_id) { |
| 111 | + log::warn!( |
| 112 | + "[Cmd Handler] Warning: Command ID '{}' is already registered. Overwriting.", |
| 113 | + command_id |
| 114 | + ); |
| 115 | + |
| 116 | + // TODO: Add ownership tracking and update/remove old owner if |
| 117 | + // overwriting |
| 118 | + } |
| 119 | + |
| 120 | + // Insert the proxy handler |
| 121 | + registry.insert( |
| 122 | + command_id.clone(), |
| 123 | + CommandHandler::Proxied { sidecar_id:sidecar_id.clone(), command_id:command_id.clone() }, |
| 124 | + ); |
| 125 | + |
| 126 | + // Keep: Confirmation of success |
| 127 | + log::info!("[Cmd Handler] Command '{}' registered successfully in AppState.", command_id); |
| 128 | + |
| 129 | + // TODO: Register ownership if implemented |
| 130 | + |
| 131 | + // Success, void operation |
| 132 | + Ok(Value::Null) |
| 133 | +} |
| 134 | + |
| 135 | +/// Handles the `commands_unregisterCommand` request from a sidecar shim. |
| 136 | +/// Removes the command ID association for the originating sidecar (if owned). |
| 137 | +pub async fn handle_unregister_command<R:TauriRuntime>( |
| 138 | + app:AppHandle<R>, |
| 139 | + |
| 140 | + sidecar_id:String, |
| 141 | + |
| 142 | + params:Value, |
| 143 | +) -> Result<Value, String> { |
| 144 | + let command_id = params |
| 145 | + .get("id") |
| 146 | + .and_then(|v| v.as_str()) |
| 147 | + .ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))?; |
| 148 | + |
| 149 | + // Keep: Essential unregistration action log |
| 150 | + log::info!( |
| 151 | + "[Cmd Handler] Unregistering command '{}' from sidecar '{}'", |
| 152 | + command_id, |
| 153 | + sidecar_id |
| 154 | + ); |
| 155 | + |
| 156 | + let app_state = app.state::<AppState>(); |
| 157 | + |
| 158 | + let mut registry = app_state.command_registry.lock().map_err(map_lock_error)?; |
| 159 | + |
| 160 | + // TODO: Implement ownership check before removing. |
| 161 | + // For now, simple removal: |
| 162 | + // Keep: Confirmation or warning log |
| 163 | + if registry.remove(command_id).is_some() { |
| 164 | + log::info!("[Cmd Handler] Command '{}' unregistered.", command_id); |
| 165 | + |
| 166 | + // TODO: Also remove from ownership map if implemented |
| 167 | + } else { |
| 168 | + log::warn!("[Cmd Handler] Command '{}' not found for unregistration.", command_id); |
| 169 | + } |
| 170 | + |
| 171 | + // Success, void operation even if not found |
| 172 | + Ok(Value::Null) |
| 173 | +} |
| 174 | + |
| 175 | +/// Handles the `commands_getCommands` request from a sidecar shim. |
| 176 | +/// Returns a list of *all* registered command IDs (native and proxied). |
| 177 | +pub async fn handle_get_commands<R:TauriRuntime>( |
| 178 | + app:AppHandle<R>, |
| 179 | + |
| 180 | + // Keep runtime state injection if needed later |
| 181 | + _runtime:State<'_, Arc<AppRuntime>>, |
| 182 | +) -> Result<Value, String> { |
| 183 | + // Reduced logging: log::trace!("[Cmd Handler] Handling getCommands request"); |
| 184 | + |
| 185 | + let app_state = app.state::<AppState>(); |
| 186 | + |
| 187 | + let registry = app_state.command_registry.lock().map_err(map_lock_error)?; |
| 188 | + |
| 189 | + let command_list:Vec<String> = registry.keys().cloned().collect(); |
| 190 | + |
| 191 | + // Release lock |
| 192 | + drop(registry); |
| 193 | + |
| 194 | + Ok(json!(command_list)) |
| 195 | +} |
| 196 | + |
| 197 | +/// Handles the `commands_executeCommand` request, typically from a sidecar shim |
| 198 | +/// or the frontend. Determines if the command is native or registered by a |
| 199 | +/// sidecar and routes accordingly. |
| 200 | +pub async fn handle_execute_command<R:TauriRuntime>( |
| 201 | + app:AppHandle<R>, |
| 202 | + |
| 203 | + // Window context might be needed for native actions |
| 204 | + window:Window<R>, |
| 205 | + |
| 206 | + // Runtime needed for executing native effects |
| 207 | + runtime:State<'_, Arc<AppRuntime>>, |
| 208 | + |
| 209 | + // Expects { "id": "command.id", "args": [...] } |
| 210 | + params:Value, |
| 211 | +) -> Result<Value, String> { |
| 212 | + let command_id = params |
| 213 | + .get("id") |
| 214 | + .and_then(|v| v.as_str()) |
| 215 | + .ok_or_else(|| create_handler_error_string("Missing or invalid 'id' parameter".to_string(), Some("EBADARG")))? |
| 216 | + .to_string(); |
| 217 | + |
| 218 | + // Default to Null if missing |
| 219 | + let args = params.get("args").cloned().unwrap_or(Value::Null); |
| 220 | + |
| 221 | + // Keep: Essential action log |
| 222 | + log::info!("[Cmd Handler] Handling executeCommand '{}'", command_id); |
| 223 | + |
| 224 | + let app_state = app.state::<AppState>(); |
| 225 | + |
| 226 | + // Get handler info while holding lock briefly |
| 227 | + let handler_info = { |
| 228 | + let registry = app_state.command_registry.lock().map_err(map_lock_error)?; |
| 229 | + |
| 230 | + registry.get(&command_id).cloned() |
| 231 | + // Lock released |
| 232 | + }; |
| 233 | + |
| 234 | + match handler_info { |
| 235 | + Some(CommandHandler::Native(native_handler_fn)) => { |
| 236 | + // Keep: Distinguishes native execution path |
| 237 | + log::debug!("[Cmd Handler] Found Native handler for '{}'. Executing...", command_id); |
| 238 | + |
| 239 | + native_handler_fn(app, window, runtime.inner().clone(), args).await |
| 240 | + }, |
| 241 | + |
| 242 | + Some(CommandHandler::Proxied { sidecar_id, command_id: proxied_cmd_id }) => { |
| 243 | + // Keep: Distinguishes proxied execution path |
| 244 | + log::debug!( |
| 245 | + "[Cmd Handler] Found Proxied handler for '{}'. Routing execution to sidecar '{}'", |
| 246 | + proxied_cmd_id, |
| 247 | + sidecar_id |
| 248 | + ); |
| 249 | + |
| 250 | + let request_payload = json!({ "id": proxied_cmd_id, "args": args }); |
| 251 | + |
| 252 | + let sidecar_method = "commands_executeContributedCommand".to_string(); |
| 253 | + |
| 254 | + vine::send_request(&sidecar_id, sidecar_method, request_payload, false, 30000) |
| 255 | + .await |
| 256 | + .map_err(|e| { |
| 257 | + log::error!( |
| 258 | + "[Cmd Handler] Vine error routing command '{}' to sidecar '{}': {}", |
| 259 | + command_id, |
| 260 | + sidecar_id, |
| 261 | + e |
| 262 | + ); |
| 263 | + |
| 264 | + create_handler_error_string( |
| 265 | + format!("Failed to route command '{}' to sidecar '{}'", command_id, sidecar_id), |
| 266 | + Some("EIPC"), |
| 267 | + ) |
| 268 | + }) |
| 269 | + }, |
| 270 | + |
| 271 | + None => { |
| 272 | + // Keep: Important error log |
| 273 | + log::error!("[Cmd Handler] Command '{}' not found in registry.", command_id); |
| 274 | + |
| 275 | + // Return structured error string |
| 276 | + Err(create_handler_error_string( |
| 277 | + format!("Command '{}' not found.", command_id), |
| 278 | + Some("ENOCMD"), |
| 279 | + // Example custom code |
| 280 | + )) |
| 281 | + }, |
| 282 | + } |
| 283 | +} |
| 284 | + |
| 285 | +// --- Native Command Handler Implementations --- |
| 286 | + |
| 287 | +/// Native handler for `workbench.action.files.saveAll`. Runs the effect. |
| 288 | +pub fn handle_native_save_all<R:TauriRuntime>( |
| 289 | + _app:AppHandle<R>, |
| 290 | + |
| 291 | + _window:Window<R>, |
| 292 | + |
| 293 | + runtime:Arc<AppRuntime>, |
| 294 | + |
| 295 | + args:Value, |
| 296 | +) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> { |
| 297 | + Box::pin(async move { |
| 298 | + let include_untitled = args.get(0).and_then(|v| v.as_bool()).unwrap_or(true); |
| 299 | + |
| 300 | + log::info!("[Native Cmd] Executing saveAll (includeUntitled: {})", include_untitled); |
| 301 | + |
| 302 | + let effect = workspace_effects::save_all(include_untitled); |
| 303 | + |
| 304 | + runtime.run(effect).await.map(|_| Value::Null).map_err(|e| e.to_string()) |
| 305 | + }) |
| 306 | +} |
| 307 | + |
| 308 | +/// Native handler for `mountain.action.showAbout`. Runs the effect. |
| 309 | +pub fn handle_native_show_about<R:TauriRuntime>( |
| 310 | + _app:AppHandle<R>, |
| 311 | + |
| 312 | + _window:Window<R>, |
| 313 | + |
| 314 | + runtime:Arc<AppRuntime>, |
| 315 | + |
| 316 | + _args:Value, |
| 317 | +) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> { |
| 318 | + Box::pin(async move { |
| 319 | + log::info!("[Native Cmd] Executing showAbout"); |
| 320 | + |
| 321 | + let message = format!("Land Editor (Mountain)\nVersion: {}\nMore info...", env!("CARGO_PKG_VERSION")); |
| 322 | + |
| 323 | + let effect = ui_effects::show_message("info".to_string(), message, None); |
| 324 | + |
| 325 | + runtime.run(effect).await.map(|_| Value::Null).map_err(|e| e.to_string()) |
| 326 | + }) |
| 327 | +} |
| 328 | + |
| 329 | +/// Placeholder native handler function. |
| 330 | +pub fn native_placeholder_handler<R:TauriRuntime>( |
| 331 | + _app:AppHandle<R>, |
| 332 | + |
| 333 | + _window:Window<R>, |
| 334 | + |
| 335 | + _runtime:Arc<AppRuntime>, |
| 336 | + |
| 337 | + args:Value, |
| 338 | +) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> { |
| 339 | + Box::pin(async move { |
| 340 | + // Keep: Useful for seeing if placeholder was called |
| 341 | + log::info!("[Native Cmd Placeholder] Executed with args: {:?}", args); |
| 342 | + |
| 343 | + Ok(json!("Native Placeholder OK")) |
| 344 | + }) |
| 345 | +} |
| 346 | + |
| 347 | +// --- Native Command Registration Helper --- |
| 348 | + |
| 349 | +/// Helper to register a native command handler map during startup (e.g., in |
| 350 | +/// `app_state.rs`). |
| 351 | +pub fn register_native_command_internal<R:TauriRuntime + 'static>( |
| 352 | + registry:&mut HashMap<String, CommandHandler<R>>, |
| 353 | + |
| 354 | + command_id:String, |
| 355 | + |
| 356 | + handler:fn( |
| 357 | + AppHandle<R>, |
| 358 | + |
| 359 | + Window<R>, |
| 360 | + |
| 361 | + Arc<AppRuntime>, |
| 362 | + |
| 363 | + Value, |
| 364 | + ) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>>, |
| 365 | +) { |
| 366 | + // Keep: Warning for collisions |
| 367 | + if registry.contains_key(&command_id) { |
| 368 | + log::warn!( |
| 369 | + "[Cmd Registry Init] Warning: Native command ID '{}' collision during registration.", |
| 370 | + command_id |
| 371 | + ); |
| 372 | + } |
| 373 | + |
| 374 | + // Keep: Log registration during init |
| 375 | + log::info!("[Cmd Registry Init] Registered native command: {}", command_id); |
| 376 | + |
| 377 | + registry.insert(command_id, CommandHandler::Native(handler)); |
| 378 | +} |
0 commit comments