mod pipes; mod plugin_loader; mod plugin_map; mod plugin_worker; mod wasm_bridge; mod watch_filesystem; mod zellij_exports; use log::info; use std::{ collections::{BTreeMap, HashMap, HashSet}, fs, path::PathBuf, time::Duration, }; use wasmtime::Engine; use crate::panes::PaneId; use crate::screen::ScreenInstruction; use crate::session_layout_metadata::SessionLayoutMetadata; use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction}; pub use wasm_bridge::PluginRenderAsset; use wasm_bridge::WasmBridge; use async_std::{channel, future::timeout, task}; use zellij_utils::{ data::{ ClientInfo, Event, EventType, InputMode, MessageToPlugin, PermissionStatus, PermissionType, PipeMessage, PipeSource, PluginCapabilities, }, errors::{prelude::*, ContextType, PluginContext}, input::{ command::TerminalAction, keybinds::Keybinds, layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout}, plugins::PluginAliases, }, ipc::ClientAttributes, pane_size::Size, session_serialization, }; pub type PluginId = u32; #[derive(Clone, Debug)] pub enum PluginInstruction { Load( Option, // should float bool, // should be opened in place Option, // pane title RunPluginOrAlias, Option, // tab index Option, // pane id to replace if this is to be opened "in-place" ClientId, Size, Option, // cwd bool, // skip cache ), LoadBackgroundPlugin(RunPluginOrAlias, ClientId), Update(Vec<(Option, Option, Event)>), // Focused plugin / broadcast, client_id, event data Unload(PluginId), // plugin_id Reload( Option, // should float Option, // pane title RunPluginOrAlias, usize, // tab index Size, ), ReloadPluginWithId(u32), Resize(PluginId, usize, usize), // plugin_id, columns, rows AddClient(ClientId), RemoveClient(ClientId), NewTab( Option, Option, Option, Vec, usize, // tab_index bool, // should change focus to new tab ClientId, ), ApplyCachedEvents { plugin_ids: Vec, done_receiving_permissions: bool, }, ApplyCachedWorkerMessages(PluginId), PostMessagesToPluginWorker( PluginId, ClientId, String, // worker name Vec<( String, // serialized message name String, // serialized payload )>, ), PostMessageToPlugin( PluginId, ClientId, String, // serialized message String, // serialized payload ), PluginSubscribedToEvents(PluginId, ClientId, HashSet), PermissionRequestResult( PluginId, Option, Vec, PermissionStatus, Option, ), DumpLayout(SessionLayoutMetadata, ClientId), ListClientsMetadata(SessionLayoutMetadata, ClientId), DumpLayoutToPlugin(SessionLayoutMetadata, PluginId), LogLayoutToHd(SessionLayoutMetadata), CliPipe { pipe_id: String, name: String, payload: Option, plugin: Option, args: Option>, configuration: Option>, floating: Option, pane_id_to_replace: Option, pane_title: Option, cwd: Option, skip_cache: bool, cli_client_id: ClientId, }, KeybindPipe { name: String, payload: Option, plugin: Option, args: Option>, configuration: Option>, floating: Option, pane_id_to_replace: Option, pane_title: Option, cwd: Option, skip_cache: bool, cli_client_id: ClientId, plugin_and_client_id: Option<(u32, ClientId)>, }, CachePluginEvents { plugin_id: PluginId, }, MessageFromPlugin { source_plugin_id: u32, message: MessageToPlugin, }, UnblockCliPipes(Vec), Reconfigure { client_id: ClientId, keybinds: Option, default_mode: Option, default_shell: Option, was_written_to_disk: bool, }, FailedToWriteConfigToDisk { file_path: Option, }, WatchFilesystem, ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId), ChangePluginHostDir(PathBuf, PluginId, ClientId), Exit, } impl From<&PluginInstruction> for PluginContext { fn from(plugin_instruction: &PluginInstruction) -> Self { match *plugin_instruction { PluginInstruction::Load(..) => PluginContext::Load, PluginInstruction::LoadBackgroundPlugin(..) => PluginContext::LoadBackgroundPlugin, PluginInstruction::Update(..) => PluginContext::Update, PluginInstruction::Unload(..) => PluginContext::Unload, PluginInstruction::Reload(..) => PluginContext::Reload, PluginInstruction::ReloadPluginWithId(..) => PluginContext::ReloadPluginWithId, PluginInstruction::Resize(..) => PluginContext::Resize, PluginInstruction::Exit => PluginContext::Exit, PluginInstruction::AddClient(_) => PluginContext::AddClient, PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, PluginInstruction::NewTab(..) => PluginContext::NewTab, PluginInstruction::ApplyCachedEvents { .. } => PluginContext::ApplyCachedEvents, PluginInstruction::ApplyCachedWorkerMessages(..) => { PluginContext::ApplyCachedWorkerMessages }, PluginInstruction::PostMessagesToPluginWorker(..) => { PluginContext::PostMessageToPluginWorker }, PluginInstruction::PostMessageToPlugin(..) => PluginContext::PostMessageToPlugin, PluginInstruction::PluginSubscribedToEvents(..) => { PluginContext::PluginSubscribedToEvents }, PluginInstruction::PermissionRequestResult(..) => { PluginContext::PermissionRequestResult }, PluginInstruction::DumpLayout(..) => PluginContext::DumpLayout, PluginInstruction::ListClientsMetadata(..) => PluginContext::ListClientsMetadata, PluginInstruction::LogLayoutToHd(..) => PluginContext::LogLayoutToHd, PluginInstruction::CliPipe { .. } => PluginContext::CliPipe, PluginInstruction::CachePluginEvents { .. } => PluginContext::CachePluginEvents, PluginInstruction::MessageFromPlugin { .. } => PluginContext::MessageFromPlugin, PluginInstruction::UnblockCliPipes { .. } => PluginContext::UnblockCliPipes, PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem, PluginInstruction::KeybindPipe { .. } => PluginContext::KeybindPipe, PluginInstruction::DumpLayoutToPlugin(..) => PluginContext::DumpLayoutToPlugin, PluginInstruction::Reconfigure { .. } => PluginContext::Reconfigure, PluginInstruction::FailedToWriteConfigToDisk { .. } => { PluginContext::FailedToWriteConfigToDisk }, PluginInstruction::ListClientsToPlugin(..) => PluginContext::ListClientsToPlugin, PluginInstruction::ChangePluginHostDir(..) => PluginContext::ChangePluginHostDir, } } } pub(crate) fn plugin_thread_main( bus: Bus, engine: Engine, data_dir: PathBuf, mut layout: Box, layout_dir: Option, path_to_default_shell: PathBuf, zellij_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, plugin_aliases: Box, default_mode: InputMode, default_keybinds: Keybinds, background_plugins: HashSet, // the client id that started the session, // we need it here because the thread's own list of connected clients might not yet be updated // on session start when we need to load the background plugins, and so we must have an // explicit client_id that has started the session initiating_client_id: ClientId, ) -> Result<()> { info!("Wasm main thread starts"); let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); layout.populate_plugin_aliases_in_layout(&plugin_aliases); // use this channel to ensure that tasks spawned from this thread terminate before exiting // https://tokio.rs/tokio/topics/shutdown#waiting-for-things-to-finish-shutting-down let (shutdown_send, shutdown_receive) = channel::bounded::<()>(1); let mut wasm_bridge = WasmBridge::new( bus.senders.clone(), engine, plugin_dir, path_to_default_shell, zellij_cwd, capabilities, client_attributes, default_shell, layout.clone(), layout_dir, default_mode, default_keybinds, ); for run_plugin_or_alias in background_plugins { load_background_plugin( run_plugin_or_alias, &mut wasm_bridge, &bus, &plugin_aliases, initiating_client_id, ); } loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { PluginInstruction::Load( should_float, should_be_open_in_place, pane_title, mut run_plugin_or_alias, tab_index, pane_id_to_replace, client_id, size, cwd, skip_cache, ) => { run_plugin_or_alias.populate_run_plugin_if_needed(&plugin_aliases); let cwd = run_plugin_or_alias.get_initial_cwd().or(cwd); let run_plugin = run_plugin_or_alias.get_run_plugin(); let start_suppressed = false; match wasm_bridge.load_plugin( &run_plugin, tab_index, size, cwd.clone(), skip_cache, Some(client_id), None, ) { Ok((plugin_id, client_id)) => { drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( should_float, should_be_open_in_place, run_plugin_or_alias, pane_title, tab_index, plugin_id, pane_id_to_replace, cwd, start_suppressed, Some(client_id), ))); }, Err(e) => { log::error!("Failed to load plugin: {e}"); }, } }, PluginInstruction::LoadBackgroundPlugin(run_plugin_or_alias, client_id) => { load_background_plugin( run_plugin_or_alias, &mut wasm_bridge, &bus, &plugin_aliases, client_id, ); }, PluginInstruction::Update(updates) => { wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, PluginInstruction::Unload(pid) => { wasm_bridge.unload_plugin(pid)?; }, PluginInstruction::Reload( should_float, pane_title, mut run_plugin_or_alias, tab_index, size, ) => { run_plugin_or_alias.populate_run_plugin_if_needed(&plugin_aliases); match run_plugin_or_alias.get_run_plugin() { Some(run_plugin) => { match wasm_bridge.reload_plugin(&run_plugin) { Ok(_) => { let _ = bus .senders .send_to_server(ServerInstruction::UnblockInputThread); }, Err(err) => match err.downcast_ref::() { Some(ZellijError::PluginDoesNotExist) => { log::warn!( "Plugin {} not found, starting it instead", run_plugin.location ); // we intentionally do not provide the client_id here because it belongs to // the cli who spawned the command and is not an existing client_id let skip_cache = true; // when reloading we always skip cache let start_suppressed = false; match wasm_bridge.load_plugin( &Some(run_plugin), Some(tab_index), size, None, skip_cache, None, None, ) { Ok((plugin_id, _client_id)) => { let should_be_open_in_place = false; drop(bus.senders.send_to_screen( ScreenInstruction::AddPlugin( should_float, should_be_open_in_place, run_plugin_or_alias, pane_title, Some(tab_index), plugin_id, None, None, start_suppressed, None, ), )); }, Err(e) => { log::error!("Failed to load plugin: {e}"); }, }; }, _ => { return Err(err); }, }, } }, None => { log::error!("Failed to find plugin info for: {:?}", run_plugin_or_alias) }, } }, PluginInstruction::ReloadPluginWithId(plugin_id) => { wasm_bridge.reload_plugin_with_id(plugin_id).non_fatal(); }, PluginInstruction::Resize(pid, new_columns, new_rows) => { wasm_bridge.resize_plugin(pid, new_columns, new_rows, shutdown_send.clone())?; }, PluginInstruction::AddClient(client_id) => { wasm_bridge.add_client(client_id)?; }, PluginInstruction::RemoveClient(client_id) => { wasm_bridge.remove_client(client_id); }, PluginInstruction::NewTab( cwd, terminal_action, mut tab_layout, mut floating_panes_layout, tab_index, should_change_focus_to_new_tab, client_id, ) => { // prefer connected clients so as to avoid opening plugins in the background for // CLI clients unless no-one else is connected let client_id = if wasm_bridge.client_is_connected(&client_id) { client_id } else if let Some(first_client_id) = wasm_bridge.get_first_client_id() { first_client_id } else { client_id }; let mut plugin_ids: HashMap> = HashMap::new(); tab_layout = tab_layout.or_else(|| Some(layout.new_tab().0)); tab_layout.as_mut().map(|t| { t.populate_plugin_aliases_in_layout(&plugin_aliases); if let Some(cwd) = cwd.as_ref() { t.add_cwd_to_layout(cwd); } t }); floating_panes_layout.iter_mut().for_each(|f| { f.run .as_mut() .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases)); }); let mut extracted_run_instructions = tab_layout .clone() .unwrap_or_else(|| layout.new_tab().0) .extract_run_instructions(); let size = Size::default(); let floating_panes_layout = if floating_panes_layout.is_empty() { layout.new_tab().1 } else { floating_panes_layout }; let mut extracted_floating_plugins: Vec> = floating_panes_layout .iter() .filter(|f| !f.already_running) .map(|f| f.run.clone()) .collect(); extracted_run_instructions.append(&mut extracted_floating_plugins); for run_instruction in extracted_run_instructions { if let Some(Run::Plugin(run_plugin_or_alias)) = run_instruction { let run_plugin = run_plugin_or_alias.get_run_plugin(); let cwd = run_plugin_or_alias .get_initial_cwd() .or_else(|| cwd.clone()); let skip_cache = false; let (plugin_id, _client_id) = wasm_bridge.load_plugin( &run_plugin, Some(tab_index), size, cwd, skip_cache, Some(client_id), None, )?; plugin_ids .entry(run_plugin_or_alias.clone()) .or_default() .push(plugin_id); } } drop(bus.senders.send_to_pty(PtyInstruction::NewTab( cwd, terminal_action, tab_layout, floating_panes_layout, tab_index, plugin_ids, should_change_focus_to_new_tab, client_id, ))); }, PluginInstruction::ApplyCachedEvents { plugin_ids, done_receiving_permissions, } => { wasm_bridge.apply_cached_events( plugin_ids, done_receiving_permissions, shutdown_send.clone(), )?; }, PluginInstruction::ApplyCachedWorkerMessages(plugin_id) => { wasm_bridge.apply_cached_worker_messages(plugin_id)?; }, PluginInstruction::PostMessagesToPluginWorker( plugin_id, client_id, worker_name, messages, ) => { wasm_bridge.post_messages_to_plugin_worker( plugin_id, client_id, worker_name, messages, )?; }, PluginInstruction::PostMessageToPlugin(plugin_id, client_id, message, payload) => { let updates = vec![( Some(plugin_id), Some(client_id), Event::CustomMessage(message, payload), )]; wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, PluginInstruction::PluginSubscribedToEvents(_plugin_id, _client_id, _events) => { // no-op, there used to be stuff we did here - now there isn't, but we might want // to add stuff here in the future }, PluginInstruction::PermissionRequestResult( plugin_id, client_id, permissions, status, cache_path, ) => { if let Err(e) = wasm_bridge.cache_plugin_permissions( plugin_id, client_id, permissions, status, cache_path, ) { log::error!("{}", e); } let updates = vec![( Some(plugin_id), client_id, Event::PermissionRequestResult(status), )]; wasm_bridge.update_plugins(updates, shutdown_send.clone())?; let done_receiving_permissions = true; wasm_bridge.apply_cached_events( vec![plugin_id], done_receiving_permissions, shutdown_send.clone(), )?; }, PluginInstruction::DumpLayout(mut session_layout_metadata, client_id) => { populate_session_layout_metadata( &mut session_layout_metadata, &wasm_bridge, &plugin_aliases, ); drop(bus.senders.send_to_pty(PtyInstruction::DumpLayout( session_layout_metadata, client_id, ))); }, PluginInstruction::ListClientsMetadata(mut session_layout_metadata, client_id) => { populate_session_layout_metadata( &mut session_layout_metadata, &wasm_bridge, &plugin_aliases, ); drop(bus.senders.send_to_pty(PtyInstruction::ListClientsMetadata( session_layout_metadata, client_id, ))); }, PluginInstruction::DumpLayoutToPlugin(mut session_layout_metadata, plugin_id) => { populate_session_layout_metadata( &mut session_layout_metadata, &wasm_bridge, &plugin_aliases, ); match session_serialization::serialize_session_layout( session_layout_metadata.into(), ) { Ok((layout, _pane_contents)) => { let updates = vec![( Some(plugin_id), None, Event::CustomMessage("session_layout".to_owned(), layout), )]; wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, Err(e) => { let updates = vec![( Some(plugin_id), None, Event::CustomMessage( "session_layout_error".to_owned(), format!("{}", e), ), )]; wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, } }, PluginInstruction::ListClientsToPlugin( mut session_layout_metadata, plugin_id, client_id, ) => { populate_session_layout_metadata( &mut session_layout_metadata, &wasm_bridge, &plugin_aliases, ); let mut clients_metadata = session_layout_metadata.all_clients_metadata(); let mut client_list_for_plugin = vec![]; let default_editor = session_layout_metadata.default_editor.clone(); for (client_metadata_id, client_metadata) in clients_metadata.iter_mut() { let is_current_client = client_metadata_id == &client_id; client_list_for_plugin.push(ClientInfo::new( *client_metadata_id, client_metadata.get_pane_id().into(), client_metadata.stringify_command(&default_editor), is_current_client, )); } let updates = vec![( Some(plugin_id), Some(client_id), Event::ListClients(client_list_for_plugin), )]; wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, PluginInstruction::LogLayoutToHd(mut session_layout_metadata) => { populate_session_layout_metadata( &mut session_layout_metadata, &wasm_bridge, &plugin_aliases, ); drop( bus.senders .send_to_pty(PtyInstruction::LogLayoutToHd(session_layout_metadata)), ); }, PluginInstruction::CliPipe { pipe_id, name, payload, plugin, args, configuration, floating, pane_id_to_replace, pane_title, cwd, skip_cache, cli_client_id, } => { let should_float = floating.unwrap_or(true); let mut pipe_messages = vec![]; match plugin { Some(plugin_url) => { // send to specific plugin(s) pipe_to_specific_plugins( PipeSource::Cli(pipe_id.clone()), &plugin_url, &configuration, &cwd, skip_cache, should_float, &pane_id_to_replace, &pane_title, Some(cli_client_id), &mut pipe_messages, &name, &payload, &args, &bus, &mut wasm_bridge, &plugin_aliases, ); }, None => { // no specific destination, send to all plugins pipe_to_all_plugins( PipeSource::Cli(pipe_id.clone()), &name, &payload, &args, &mut wasm_bridge, &mut pipe_messages, ); }, } wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?; }, PluginInstruction::KeybindPipe { name, payload, plugin, args, configuration, floating, pane_id_to_replace, pane_title, cwd, skip_cache, cli_client_id, plugin_and_client_id, } => { let should_float = floating.unwrap_or(true); let mut pipe_messages = vec![]; if let Some((plugin_id, client_id)) = plugin_and_client_id { let is_private = true; pipe_messages.push(( Some(plugin_id), Some(client_id), PipeMessage::new(PipeSource::Keybind, name, &payload, &args, is_private), )); } else { match plugin { Some(plugin_url) => { // send to specific plugin(s) pipe_to_specific_plugins( PipeSource::Keybind, &plugin_url, &configuration, &cwd, skip_cache, should_float, &pane_id_to_replace, &pane_title, Some(cli_client_id), &mut pipe_messages, &name, &payload, &args, &bus, &mut wasm_bridge, &plugin_aliases, ); }, None => { // no specific destination, send to all plugins pipe_to_all_plugins( PipeSource::Keybind, &name, &payload, &args, &mut wasm_bridge, &mut pipe_messages, ); }, } } wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?; }, PluginInstruction::CachePluginEvents { plugin_id } => { wasm_bridge.cache_plugin_events(plugin_id); }, PluginInstruction::MessageFromPlugin { source_plugin_id, message, } => { let mut pipe_messages = vec![]; let skip_cache = message .new_plugin_args .as_ref() .map(|n| n.skip_cache) .unwrap_or(false); let should_float = message .new_plugin_args .as_ref() .and_then(|n| n.should_float) .unwrap_or(true); let pane_title = message .new_plugin_args .as_ref() .and_then(|n| n.pane_title.clone()); let pane_id_to_replace = message .new_plugin_args .as_ref() .and_then(|n| n.pane_id_to_replace); match (message.plugin_url, message.destination_plugin_id) { (Some(plugin_url), None) => { // send to specific plugin(s) pipe_to_specific_plugins( PipeSource::Plugin(source_plugin_id), &plugin_url, &Some(message.plugin_config), &None, skip_cache, should_float, &pane_id_to_replace.map(|p| p.into()), &pane_title, None, &mut pipe_messages, &message.message_name, &message.message_payload, &Some(message.message_args), &bus, &mut wasm_bridge, &plugin_aliases, ); }, (None, Some(destination_plugin_id)) => { let is_private = true; pipe_messages.push(( Some(destination_plugin_id), None, PipeMessage::new( PipeSource::Plugin(source_plugin_id), message.message_name, &message.message_payload, &Some(message.message_args), is_private, ), )); }, (Some(plugin_url), Some(destination_plugin_id)) => { log::warn!("Message contains both a destination plugin url: {plugin_url} and a destination plugin id: {destination_plugin_id}, ignoring the url and prioritizing the id"); let is_private = true; pipe_messages.push(( Some(destination_plugin_id), None, PipeMessage::new( PipeSource::Plugin(source_plugin_id), message.message_name, &message.message_payload, &Some(message.message_args), is_private, ), )); }, (None, None) => { // send to all plugins pipe_to_all_plugins( PipeSource::Plugin(source_plugin_id), &message.message_name, &message.message_payload, &Some(message.message_args), &mut wasm_bridge, &mut pipe_messages, ); }, } wasm_bridge.pipe_messages(pipe_messages, shutdown_send.clone())?; }, PluginInstruction::UnblockCliPipes(pipes_to_unblock) => { let pipes_to_unblock = wasm_bridge.update_cli_pipe_state(pipes_to_unblock); for pipe_name in pipes_to_unblock { let _ = bus .senders .send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name)) .context("failed to unblock input pipe"); } }, PluginInstruction::Reconfigure { client_id, keybinds, default_mode, default_shell, was_written_to_disk, } => { wasm_bridge .reconfigure(client_id, keybinds, default_mode, default_shell) .non_fatal(); // TODO: notify plugins that this happened so that they can eg. rebind temporary keys that // were lost if was_written_to_disk { let updates = vec![(None, None, Event::ConfigWasWrittenToDisk)]; wasm_bridge .update_plugins(updates, shutdown_send.clone()) .non_fatal(); } }, PluginInstruction::FailedToWriteConfigToDisk { file_path } => { let updates = vec![( None, None, Event::FailedToWriteConfigToDisk(file_path.map(|f| f.display().to_string())), )]; wasm_bridge .update_plugins(updates, shutdown_send.clone()) .non_fatal(); }, PluginInstruction::WatchFilesystem => { wasm_bridge.start_fs_watcher_if_not_started(); }, PluginInstruction::ChangePluginHostDir(new_host_folder, plugin_id, client_id) => { wasm_bridge .change_plugin_host_dir(new_host_folder, plugin_id, client_id) .non_fatal(); }, PluginInstruction::Exit => { break; }, } } info!("wasm main thread exits"); // first drop our sender, then call recv. // once all senders are dropped or the timeout is reached, recv will return an error, that we ignore drop(shutdown_send); task::block_on(async { let result = timeout(EXIT_TIMEOUT, shutdown_receive.recv()).await; if let Err(err) = result { log::error!("timeout waiting for plugin tasks to finish: {}", err); } }); wasm_bridge.cleanup(); fs::remove_dir_all(&plugin_global_data_dir) .or_else(|err| { if err.kind() == std::io::ErrorKind::NotFound { // I don't care... Ok(()) } else { Err(err) } }) .context("failed to cleanup plugin data directory") } fn populate_session_layout_metadata( session_layout_metadata: &mut SessionLayoutMetadata, wasm_bridge: &WasmBridge, plugin_aliases: &PluginAliases, ) { let plugin_ids = session_layout_metadata.all_plugin_ids(); let mut plugin_ids_to_cmds: HashMap = HashMap::new(); for plugin_id in plugin_ids { let plugin_cmd = wasm_bridge.run_plugin_of_plugin_id(plugin_id); match plugin_cmd { Some(plugin_cmd) => { plugin_ids_to_cmds.insert(plugin_id, plugin_cmd.clone()); }, None => log::error!("Plugin with id: {plugin_id} not found"), } } session_layout_metadata.update_plugin_cmds(plugin_ids_to_cmds); session_layout_metadata.update_plugin_aliases_in_default_layout(plugin_aliases); } fn pipe_to_all_plugins( pipe_source: PipeSource, name: &str, payload: &Option, args: &Option>, wasm_bridge: &mut WasmBridge, pipe_messages: &mut Vec<(Option, Option, PipeMessage)>, ) { let is_private = false; let all_plugin_ids = wasm_bridge.all_plugin_ids(); for (plugin_id, client_id) in all_plugin_ids { pipe_messages.push(( Some(plugin_id), Some(client_id), PipeMessage::new(pipe_source.clone(), name, payload, &args, is_private), )); } } fn pipe_to_specific_plugins( pipe_source: PipeSource, plugin_url: &str, configuration: &Option>, cwd: &Option, skip_cache: bool, should_float: bool, pane_id_to_replace: &Option, pane_title: &Option, cli_client_id: Option, pipe_messages: &mut Vec<(Option, Option, PipeMessage)>, name: &str, payload: &Option, args: &Option>, bus: &Bus, wasm_bridge: &mut WasmBridge, plugin_aliases: &PluginAliases, ) { let is_private = true; let size = Size::default(); match RunPluginOrAlias::from_url( &plugin_url, configuration, Some(plugin_aliases), cwd.clone(), ) { Ok(run_plugin_or_alias) => { let initial_cwd = run_plugin_or_alias.get_initial_cwd(); let all_plugin_ids = wasm_bridge.get_or_load_plugins( run_plugin_or_alias, size, initial_cwd.or_else(|| cwd.clone()), skip_cache, should_float, pane_id_to_replace.is_some(), pane_title.clone(), pane_id_to_replace.clone(), cli_client_id, ); for (plugin_id, client_id) in all_plugin_ids { pipe_messages.push(( Some(plugin_id), client_id, PipeMessage::new(pipe_source.clone(), name, payload, args, is_private), )); } }, Err(e) => match cli_client_id { Some(cli_client_id) => { let _ = bus.senders.send_to_server(ServerInstruction::LogError( vec![format!("Failed to parse plugin url: {}", e)], cli_client_id, )); }, None => { log::error!("Failed to parse plugin url: {}", e); }, }, } } fn load_background_plugin( mut run_plugin_or_alias: RunPluginOrAlias, wasm_bridge: &mut WasmBridge, bus: &Bus, plugin_aliases: &PluginAliases, client_id: ClientId, ) { run_plugin_or_alias.populate_run_plugin_if_needed(&plugin_aliases); let cwd = run_plugin_or_alias.get_initial_cwd(); let run_plugin = run_plugin_or_alias.get_run_plugin(); let size = Size::default(); let skip_cache = false; match wasm_bridge.load_plugin( &run_plugin, None, size, cwd.clone(), skip_cache, Some(client_id), None, ) { Ok((plugin_id, client_id)) => { let should_float = None; let should_be_open_in_place = false; let pane_title = None; let pane_id_to_replace = None; let start_suppressed = true; drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( should_float, should_be_open_in_place, run_plugin_or_alias, pane_title, None, plugin_id, pane_id_to_replace, cwd, start_suppressed, // None, Some(client_id), ))); }, Err(e) => { log::error!("Failed to load plugin: {e}"); }, } } const EXIT_TIMEOUT: Duration = Duration::from_secs(3); #[path = "./unit/plugin_tests.rs"] #[cfg(test)] mod plugin_tests;