use super::PluginInstruction; use crate::background_jobs::BackgroundJob; use crate::global_async_runtime::get_tokio_runtime; use crate::plugins::plugin_map::PluginEnv; use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::pty::{ClientTabIndexOrPaneId, PtyInstruction}; use crate::route::{route_action, wait_for_action_completion, NotificationEnd}; use crate::ServerInstruction; use log::warn; use serde::Serialize; use std::{ collections::{BTreeMap, HashSet}, io::{Read, Write}, path::PathBuf, process, str::FromStr, thread, time::{Duration, Instant}, }; use tokio::sync::oneshot; use wasmi::{Caller, Linker}; use zellij_utils::consts::ipc_connect; use zellij_utils::data::{ BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse, CommandType, ConnectToSession, DeleteAllDeadSessionsResponse, DeleteDeadSessionResponse, DeleteLayoutResponse, EditLayoutResponse, Event, FloatingPaneCoordinates, FocusOrCreateTabResponse, GetFocusedPaneInfoResponse, GetPaneCwdResponse, GetPanePidResponse, GetPaneRunningCommandResponse, HttpVerb, KeyWithModifier, KillSessionsResponse, LayoutInfo, LayoutMetadata, LayoutParsingError, MessageToPlugin, NewPanePlacement, NewTabResponse, NewTabUnfocusedResponse, NewTiledPaneInTabResponse, OpenCommandPaneBackgroundResponse, OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse, OpenCommandPaneInPlaceOfPaneIdResponse, OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, OpenEditPaneInPlaceOfPaneIdResponse, OpenFileFloatingNearPluginResponse, OpenFileFloatingResponse, OpenFileInPlaceOfPluginResponse, OpenFileInPlaceResponse, OpenFileNearPluginResponse, OpenFileResponse, OpenPaneInNewTabResponse, OpenPluginPaneFloatingResponse, OpenTerminalFloatingNearPluginResponse, OpenTerminalFloatingResponse, OpenTerminalInPlaceOfPluginResponse, OpenTerminalInPlaceResponse, OpenTerminalNearPluginResponse, OpenTerminalPaneInPlaceOfPaneIdResponse, OpenTerminalResponse, OriginatingPlugin, PaneFrameStyle, PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission, RegexHighlight, RenameLayoutResponse, SaveLayoutResponse, TabMetadata, }; use zellij_utils::home::default_layout_dir; use zellij_utils::input::permission::PermissionCache; use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext}; use zellij_utils::sessions::generate_random_name as generate_random_name_impl; #[cfg(feature = "web_server_capability")] use zellij_utils::web_authentication_tokens::{ create_token, list_tokens, rename_token, revoke_all_tokens, revoke_token, }; #[cfg(feature = "web_server_capability")] use zellij_utils::web_server_commands::shutdown_all_webserver_instances; use crate::{panes::PaneId, screen::ScreenInstruction}; use kdl::KdlDocument; use zellij_utils::pane_size::Size; use prost::Message; use zellij_utils::{ consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, ZELLIJ_TMP_DIR}, data::{ CommandOrPlugin, CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, PluginMessage, Resize, ResizeStrategy, }, errors::prelude::*, input::{ actions::Action, command::{OpenFilePayload, RunCommand, RunCommandAction, TerminalAction}, config::ConfigError, layout::{Layout, RunPluginOrAlias, TabLayoutInfo}, }, plugin_api::{ event::{ layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType, ProtobufLayoutParsingError, ProtobufPaneScrollbackResponse, ProtobufSyntaxError, }, plugin_command::{ dump_layout_response, dump_session_layout_response, hide_floating_panes_response, parse_layout_response, save_session_response, show_floating_panes_response, ProtobufBreakPanesToNewTabResponse, ProtobufBreakPanesToTabWithIdResponse, ProtobufBreakPanesToTabWithIndexResponse, ProtobufDeleteAllDeadSessionsResponse, ProtobufDeleteDeadSessionResponse, ProtobufDeleteLayoutResponse, ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse, ProtobufFocusOrCreateTabResponse, ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse, ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse, ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse, ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetSessionListResponse, ProtobufGetTabInfoResponse, ProtobufHideFloatingPanesResponse, ProtobufKillSessionsResponse, ProtobufNewTabResponse, ProtobufNewTabUnfocusedResponse, ProtobufNewTabsResponse, ProtobufNewTiledPaneInTabResponse, ProtobufOpenCommandPaneBackgroundResponse, ProtobufOpenCommandPaneFloatingNearPluginResponse, ProtobufOpenCommandPaneFloatingResponse, ProtobufOpenCommandPaneInPlaceOfPaneIdResponse, ProtobufOpenCommandPaneInPlaceOfPluginResponse, ProtobufOpenCommandPaneInPlaceResponse, ProtobufOpenCommandPaneNearPluginResponse, ProtobufOpenCommandPaneResponse, ProtobufOpenEditPaneInPlaceOfPaneIdResponse, ProtobufOpenFileFloatingNearPluginResponse, ProtobufOpenFileFloatingResponse, ProtobufOpenFileInPlaceOfPluginResponse, ProtobufOpenFileInPlaceResponse, ProtobufOpenFileNearPluginResponse, ProtobufOpenFileResponse, ProtobufOpenPaneInNewTabResponse, ProtobufOpenPluginPaneFloatingResponse, ProtobufOpenTerminalFloatingNearPluginResponse, ProtobufOpenTerminalFloatingResponse, ProtobufOpenTerminalInPlaceOfPluginResponse, ProtobufOpenTerminalInPlaceResponse, ProtobufOpenTerminalNearPluginResponse, ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse, ProtobufOpenTerminalResponse, ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse, ProtobufShowFloatingPanesResponse, }, plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}, }, }; #[cfg(feature = "web_server_capability")] use zellij_utils::plugin_api::plugin_command::{ CreateTokenResponse, ListTokensResponse, RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse, }; macro_rules! apply_action { ($action:ident, $error_message:ident, $env: ident) => { match route_action( $action, $env.client_id, None, Some(PaneId::Plugin($env.plugin_id)), $env.senders.clone(), $env.default_shell.clone(), None, $env.default_mode.clone(), None, ) { Ok((_, result)) => result, Err(e) => { log::error!("{}: {:?}", $error_message(), e); None }, } }; } fn translate_plugin_path(env: &PluginEnv, path: PathBuf) -> PathBuf { if let Ok(stripped) = path.strip_prefix("/host") { env.plugin_cwd.join(stripped) } else if let Ok(stripped) = path.strip_prefix("/data") { env.plugin_own_data_dir.join(stripped) } else if let Ok(stripped) = path.strip_prefix("/cache") { env.plugin_own_cache_dir.join(stripped) } else if let Ok(stripped) = path.strip_prefix("/tmp") { ZELLIJ_TMP_DIR.join(stripped) } else if path.is_relative() { env.plugin_cwd.join(path) } else { // Absolute path not in any plugin special folder — pass through path } } pub fn zellij_exports(linker: &mut Linker) { linker .func_wrap("zellij", "host_run_plugin_command", host_run_plugin_command) .unwrap(); } fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { let mut env = caller.data_mut(); let plugin_command = env.name(); let err_context = || format!("failed to run plugin command {}", plugin_command); wasi_read_bytes(env) .and_then(|bytes| { let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?; let command: PluginCommand = command .try_into() .map_err(|e| anyhow!("failed to convert serialized command: {}", e))?; match check_command_permission(&env, &command) { (PermissionStatus::Granted, _) => match command { PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?, PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?, PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable), PluginCommand::ShowCursor(cursor_position) => show_cursor(env, cursor_position), PluginCommand::GetPluginIds => get_plugin_ids(env), PluginCommand::GetZellijVersion => get_zellij_version(env), PluginCommand::GenerateRandomName => generate_random_name(env), PluginCommand::DumpLayout(layout_name) => dump_layout(env, layout_name), PluginCommand::ParseLayout(layout_string) => parse_layout(env, layout_string), PluginCommand::GetLayoutDir => get_layout_dir(env), PluginCommand::GetFocusedPaneInfo => get_focused_pane_info(env), PluginCommand::SaveSession => save_session(env), PluginCommand::CurrentSessionLastSavedTime => { current_session_last_saved_time(env) }, PluginCommand::GetPaneInfo(pane_id) => get_pane_info(env, pane_id), PluginCommand::GetTabInfo(tab_id) => get_tab_info(env, tab_id), PluginCommand::OpenFile(file_to_open, context) => { open_file(env, file_to_open, context) }, PluginCommand::OpenFileFloating( file_to_open, floating_pane_coordinates, context, ) => open_file_floating(env, file_to_open, floating_pane_coordinates, context), PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?), PluginCommand::OpenTerminalNearPlugin(cwd) => { open_terminal_near_plugin(env, cwd.path.try_into()?) }, PluginCommand::OpenTerminalFloating(cwd, floating_pane_coordinates) => { open_terminal_floating(env, cwd.path.try_into()?, floating_pane_coordinates) }, PluginCommand::OpenTerminalFloatingNearPlugin( cwd, floating_pane_coordinates, ) => open_terminal_floating_near_plugin( env, cwd.path.try_into()?, floating_pane_coordinates, ), PluginCommand::OpenCommandPane(command_to_run, context) => { open_command_pane(env, command_to_run, context) }, PluginCommand::OpenCommandPaneNearPlugin(command_to_run, context) => { open_command_pane_near_plugin(env, command_to_run, context) }, PluginCommand::OpenCommandPaneFloating( command_to_run, floating_pane_coordinates, context, ) => open_command_pane_floating( env, command_to_run, floating_pane_coordinates, context, ), PluginCommand::OpenCommandPaneFloatingNearPlugin( command_to_run, floating_pane_coordinates, context, ) => open_command_pane_floating_near_plugin( env, command_to_run, floating_pane_coordinates, context, ), PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index), PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds), PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line), PluginCommand::RunCommand(command_line, env_variables, cwd, context) => { run_command(env, command_line, env_variables, cwd, context) }, PluginCommand::WebRequest(url, verb, headers, body, context) => { web_request(env, url, verb, headers, body, context) }, PluginCommand::PostMessageTo(plugin_message) => { post_message_to(env, plugin_message)? }, PluginCommand::PostMessageToPlugin(plugin_message) => { post_message_to_plugin(env, plugin_message)? }, PluginCommand::HideSelf => hide_self(env)?, PluginCommand::ShowSelf(should_float_if_hidden) => { show_self(env, should_float_if_hidden) }, PluginCommand::SwitchToMode(input_mode) => { switch_to_mode(env, input_mode.try_into()?) }, PluginCommand::NewTabsWithLayout(raw_layout) => { new_tabs_with_layout(env, &raw_layout)? }, PluginCommand::NewTabsWithLayoutInfo(layout_info) => { new_tabs_with_layout_info(env, layout_info)? }, PluginCommand::OverrideLayout( layout_info, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_active_tab, context, ) => override_layout( env, layout_info, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_active_tab, context, )?, PluginCommand::SaveLayout { layout_name, layout_kdl, overwrite, } => save_layout(env, layout_name, layout_kdl, overwrite), PluginCommand::DeleteLayout { layout_name } => delete_layout(env, layout_name), PluginCommand::RenameLayout { old_layout_name, new_layout_name, } => rename_layout(env, old_layout_name, new_layout_name), PluginCommand::EditLayout { layout_name, context, } => edit_layout(env, layout_name, context), PluginCommand::NewTab { name, cwd } => new_tab(env, name, cwd), PluginCommand::NewTabUnfocused { name, cwd } => { new_tab_unfocused(env, name, cwd) }, PluginCommand::NewTiledPaneInTab { tab_position } => { new_tiled_pane_in_tab(env, tab_position) }, PluginCommand::ToggleFloatingPanes { tab_id } => { toggle_floating_panes(env, tab_id) }, PluginCommand::NewPane => new_pane(env), PluginCommand::GoToNextTab => go_to_next_tab(env), PluginCommand::GoToPreviousTab => go_to_previous_tab(env), PluginCommand::Resize(resize_payload) => resize(env, resize_payload), PluginCommand::ResizeWithDirection(resize_strategy) => { resize_with_direction(env, resize_strategy) }, PluginCommand::FocusNextPane => focus_next_pane(env), PluginCommand::FocusPreviousPane => focus_previous_pane(env), PluginCommand::FocusLastPane => focus_last_pane(env), PluginCommand::MoveFocus(direction) => move_focus(env, direction), PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction), PluginCommand::Detach => detach(env), PluginCommand::EditScrollback => edit_scrollback(env), PluginCommand::Write(bytes) => write(env, bytes), PluginCommand::WriteChars(chars) => write_chars(env, chars), PluginCommand::CopyToClipboard(text) => copy_to_clipboard(env, text)?, PluginCommand::ToggleTab => toggle_tab(env), PluginCommand::MovePane => move_pane(env), PluginCommand::MovePaneWithDirection(direction) => { move_pane_with_direction(env, direction) }, PluginCommand::ClearScreen => clear_screen(env), PluginCommand::ScrollUp => scroll_up(env), PluginCommand::ScrollDown => scroll_down(env), PluginCommand::ScrollToTop => scroll_to_top(env), PluginCommand::ScrollToBottom => scroll_to_bottom(env), PluginCommand::PageScrollUp => page_scroll_up(env), PluginCommand::PageScrollDown => page_scroll_down(env), PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env), PluginCommand::ToggleFocusNoUiFullscreen => toggle_focus_no_ui_fullscreen(env), PluginCommand::FocusHostSession => focus_host_session(env), PluginCommand::TogglePaneFrames => toggle_pane_frames(env), PluginCommand::SetPaneFrameStyle(pane_frame_style) => { set_pane_frame_style(env, pane_frame_style) }, PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env), PluginCommand::UndoRenamePane => undo_rename_pane(env), PluginCommand::CloseFocus => close_focus(env), PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env), PluginCommand::CloseFocusedTab => close_focused_tab(env), PluginCommand::UndoRenameTab => undo_rename_tab(env), PluginCommand::QuitZellij => quit_zellij(env), PluginCommand::PreviousSwapLayout => previous_swap_layout(env), PluginCommand::NextSwapLayout => next_swap_layout(env), PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name), PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name), PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index), PluginCommand::StartOrReloadPlugin(plugin_url) => { start_or_reload_plugin(env, &plugin_url)? }, PluginCommand::CloseTerminalPane(terminal_pane_id) => { close_terminal_pane(env, terminal_pane_id) }, PluginCommand::ClosePluginPane(plugin_pane_id) => { close_plugin_pane(env, plugin_pane_id) }, PluginCommand::FocusTerminalPane( terminal_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, ) => focus_terminal_pane( env, terminal_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, ), PluginCommand::FocusPluginPane( plugin_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, ) => focus_plugin_pane( env, plugin_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, ), PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => { rename_terminal_pane(env, terminal_pane_id, &new_name) }, PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => { rename_plugin_pane(env, plugin_pane_id, &new_name) }, PluginCommand::RenameTab(tab_index, new_name) => { rename_tab(env, tab_index, &new_name) }, PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload), PluginCommand::RequestPluginPermissions(permissions) => { request_permission(env, permissions)? }, PluginCommand::SwitchSession(connect_to_session) => switch_session( env, connect_to_session.name, connect_to_session.tab_position, connect_to_session.pane_id, connect_to_session.layout, connect_to_session.cwd, )?, PluginCommand::DeleteDeadSession(session_name) => { delete_dead_session(session_name)? }, PluginCommand::DeleteAllDeadSessions => delete_all_dead_sessions()?, PluginCommand::OpenFileInPlace(file_to_open, context) => { open_file_in_place(env, file_to_open, context) }, PluginCommand::OpenTerminalInPlace(cwd) => { open_terminal_in_place(env, cwd.path.try_into()?) }, PluginCommand::OpenTerminalInPlaceOfPlugin(cwd, close_plugin_after_replace) => { open_terminal_in_place_of_plugin( env, cwd.path.try_into()?, close_plugin_after_replace, ) }, PluginCommand::OpenCommandPaneInPlace(command_to_run, context) => { open_command_pane_in_place(env, command_to_run, context) }, PluginCommand::OpenCommandPaneInPlaceOfPlugin( command_to_run, close_plugin_after_replace, context, ) => open_command_pane_in_place_of_plugin( env, command_to_run, close_plugin_after_replace, context, ), PluginCommand::RenameSession(new_session_name) => { rename_session(env, new_session_name) }, PluginCommand::UnblockCliPipeInput(pipe_name) => { unblock_cli_pipe_input(env, pipe_name) }, PluginCommand::BlockCliPipeInput(pipe_name) => { block_cli_pipe_input(env, pipe_name) }, PluginCommand::CliPipeOutput(pipe_name, output) => { cli_pipe_output(env, pipe_name, output)? }, PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?, PluginCommand::DisconnectOtherClients => disconnect_other_clients(env), PluginCommand::KillSessions(session_list) => kill_sessions(session_list), PluginCommand::KillSessionsAndReply(session_list) => { kill_sessions_and_reply(env, session_list) }, PluginCommand::DeleteDeadSessionAndReply(session_name) => { delete_dead_session_and_reply(env, session_name) }, PluginCommand::DeleteAllDeadSessionsAndReply => { delete_all_dead_sessions_and_reply(env) }, PluginCommand::ScanHostFolder(folder_to_scan) => { scan_host_folder(env, folder_to_scan) }, PluginCommand::WatchFilesystem => watch_filesystem(env), PluginCommand::ListWindowsVolumes => list_windows_volumes(env), PluginCommand::GetSessionList => get_session_list(env), PluginCommand::SetSoftKeyboard(on) => set_soft_keyboard(env, on), PluginCommand::SetTabFit { tab_id, fit } => set_tab_fit( env, tab_id, fit.map(|(pane_id, size)| (pane_id.into(), size)), ), PluginCommand::ExitMobileMode => exit_mobile_mode(env), PluginCommand::SetShadowFocus(pane_id) => set_shadow_focus(env, pane_id.into()), PluginCommand::DumpSessionLayout { tab_index } => { dump_session_layout(env, tab_index) }, PluginCommand::CloseSelf => close_self(env), PluginCommand::Reconfigure(new_config, write_config_to_disk) => { reconfigure(env, new_config, write_config_to_disk)? }, PluginCommand::HidePaneWithId(pane_id) => { hide_pane_with_id(env, pane_id.into())? }, PluginCommand::ShowPaneWithId( pane_id, should_float_if_hidden, should_focus_pane, ) => show_pane_with_id( env, pane_id.into(), should_float_if_hidden, should_focus_pane, ), PluginCommand::OpenCommandPaneBackground(command_to_run, context) => { open_command_pane_background(env, command_to_run, context) }, PluginCommand::RerunCommandPane(terminal_pane_id) => { rerun_command_pane(env, terminal_pane_id) }, PluginCommand::ResizePaneIdWithDirection(resize, pane_id) => { resize_pane_with_id(env, resize, pane_id.into()) }, PluginCommand::EditScrollbackForPaneWithId(pane_id) => { edit_scrollback_for_pane_with_id(env, pane_id.into()) }, PluginCommand::GetPaneScrollback { pane_id, get_full_scrollback, } => get_pane_scrollback(env, pane_id.into(), get_full_scrollback), PluginCommand::WriteToPaneId(bytes, pane_id) => { write_to_pane_id(env, bytes, pane_id.into()) }, PluginCommand::WriteCharsToPaneId(chars, pane_id) => { write_chars_to_pane_id(env, chars, pane_id.into()) }, PluginCommand::SendSigintToPaneId(pane_id) => { send_sigint_to_pane_id(env, pane_id.into()) }, PluginCommand::SendSigkillToPaneId(pane_id) => { send_sigkill_to_pane_id(env, pane_id.into()) }, PluginCommand::GetPanePid { pane_id } => get_pane_pid(env, pane_id.into()), PluginCommand::GetPaneRunningCommand { pane_id } => { get_pane_running_command(env, pane_id.into()) }, PluginCommand::GetPaneCwd { pane_id } => get_pane_cwd(env, pane_id.into()), PluginCommand::MovePaneWithPaneId(pane_id) => { move_pane_with_pane_id(env, pane_id.into()) }, PluginCommand::MovePaneWithPaneIdInDirection(pane_id, direction) => { move_pane_with_pane_id_in_direction(env, pane_id.into(), direction) }, PluginCommand::ClearScreenForPaneId(pane_id) => { clear_screen_for_pane_id(env, pane_id.into()) }, PluginCommand::ScrollUpInPaneId(pane_id) => { scroll_up_in_pane_id(env, pane_id.into()) }, PluginCommand::ScrollDownInPaneId(pane_id) => { scroll_down_in_pane_id(env, pane_id.into()) }, PluginCommand::ScrollToTopInPaneId(pane_id) => { scroll_to_top_in_pane_id(env, pane_id.into()) }, PluginCommand::ScrollToBottomInPaneId(pane_id) => { scroll_to_bottom_in_pane_id(env, pane_id.into()) }, PluginCommand::PageScrollUpInPaneId(pane_id) => { page_scroll_up_in_pane_id(env, pane_id.into()) }, PluginCommand::PageScrollDownInPaneId(pane_id) => { page_scroll_down_in_pane_id(env, pane_id.into()) }, PluginCommand::TogglePaneIdFullscreen(pane_id) => { toggle_pane_id_fullscreen(env, pane_id.into()) }, PluginCommand::TogglePaneEmbedOrEjectForPaneId(pane_id) => { toggle_pane_embed_or_eject_for_pane_id(env, pane_id.into()) }, PluginCommand::CloseTabWithIndex(tab_index) => { close_tab_with_index(env, tab_index) }, PluginCommand::BreakPanesToNewTab( pane_ids, new_tab_name, should_change_focus_to_new_tab, ) => break_panes_to_new_tab( env, pane_ids.into_iter().map(|p_id| p_id.into()).collect(), new_tab_name, should_change_focus_to_new_tab, ), PluginCommand::BreakPanesToTabWithIndex( pane_ids, should_change_focus_to_new_tab, tab_index, ) => break_panes_to_tab_with_index( env, pane_ids.into_iter().map(|p_id| p_id.into()).collect(), tab_index, should_change_focus_to_new_tab, ), PluginCommand::SwitchTabToId(tab_id) => switch_tab_to_id(env, tab_id), PluginCommand::GoToTabWithId(tab_id) => go_to_tab_with_id(env, tab_id), PluginCommand::CloseTabWithId(tab_id) => close_tab_with_id(env, tab_id), PluginCommand::RenameTabWithId(tab_id, new_name) => { rename_tab_with_id(env, tab_id, &new_name) }, PluginCommand::BreakPanesToTabWithId(pane_ids, tab_id, should_change_focus) => { break_panes_to_tab_with_id( env, pane_ids.into_iter().map(|p_id| p_id.into()).collect(), should_change_focus, tab_id, ) }, PluginCommand::ReloadPlugin(plugin_id) => reload_plugin(env, plugin_id), PluginCommand::LoadNewPlugin { url, config, load_in_background, skip_plugin_cache, } => load_new_plugin(env, url, config, load_in_background, skip_plugin_cache), PluginCommand::RebindKeys { keys_to_rebind, keys_to_unbind, write_config_to_disk, } => rebind_keys(env, keys_to_rebind, keys_to_unbind, write_config_to_disk)?, PluginCommand::ListClients => list_clients(env), PluginCommand::ChangeHostFolder(new_host_folder) => { change_host_folder(env, new_host_folder) }, PluginCommand::SetFloatingPanePinned(pane_id, should_be_pinned) => { set_floating_pane_pinned(env, pane_id.into(), should_be_pinned) }, PluginCommand::StackPanes(pane_ids) => { stack_panes(env, pane_ids.into_iter().map(|p_id| p_id.into()).collect()) }, PluginCommand::ChangeFloatingPanesCoordinates(pane_ids_and_coordinates) => { change_floating_panes_coordinates( env, pane_ids_and_coordinates .into_iter() .map(|(p_id, coordinates)| (p_id.into(), coordinates)) .collect(), ) }, PluginCommand::TogglePaneBorderless(pane_id) => { toggle_pane_borderless(env, pane_id.into()) }, PluginCommand::SetPaneBorderless(pane_id, borderless) => { set_pane_borderless(env, pane_id.into(), borderless) }, PluginCommand::SetPaneColor(pane_id, fg, bg) => { set_pane_color(env, pane_id.into(), fg, bg) }, PluginCommand::OpenFileNearPlugin(file_to_open, context) => { open_file_near_plugin(env, file_to_open, context) }, PluginCommand::OpenFileFloatingNearPlugin( file_to_open, floating_pane_coordinates, context, ) => open_file_floating_near_plugin( env, file_to_open, floating_pane_coordinates, context, ), PluginCommand::OpenFileInPlaceOfPlugin( file_to_open, close_plugin_after_replace, context, ) => open_file_in_place_of_plugin( env, file_to_open, close_plugin_after_replace, context, ), PluginCommand::GroupAndUngroupPanes( panes_to_group, panes_to_ungroup, for_all_clients, ) => group_and_ungroup_panes( env, panes_to_group.into_iter().map(|p| p.into()).collect(), panes_to_ungroup.into_iter().map(|p| p.into()).collect(), for_all_clients, ), PluginCommand::HighlightAndUnhighlightPanes( panes_to_highlight, panes_to_unhighlight, ) => highlight_and_unhighlight_panes( env, panes_to_highlight.into_iter().map(|p| p.into()).collect(), panes_to_unhighlight.into_iter().map(|p| p.into()).collect(), ), PluginCommand::CloseMultiplePanes(pane_ids) => { close_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect()) }, PluginCommand::FloatMultiplePanes(pane_ids) => { float_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect()) }, PluginCommand::EmbedMultiplePanes(pane_ids) => { embed_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect()) }, PluginCommand::StartWebServer => start_web_server(env), PluginCommand::StopWebServer => stop_web_server(env), PluginCommand::QueryWebServerStatus => query_web_server_status(env), PluginCommand::ShareCurrentSession => share_current_session(env), PluginCommand::StopSharingCurrentSession => stop_sharing_current_session(env), PluginCommand::SetSelfMouseSelectionSupport(selection_support) => { set_self_mouse_selection_support(env, selection_support); }, PluginCommand::GenerateWebLoginToken(token_label, read_only) => { generate_web_login_token(env, token_label, read_only); }, PluginCommand::RevokeWebLoginToken(label) => { revoke_web_login_token(env, label); }, PluginCommand::ListWebLoginTokens => { list_web_login_tokens(env); }, PluginCommand::RevokeAllWebLoginTokens => { revoke_all_web_login_tokens(env); }, PluginCommand::RenameWebLoginToken(old_name, new_name) => { rename_web_login_token(env, old_name, new_name); }, PluginCommand::InterceptKeyPresses => intercept_key_presses(&mut env), PluginCommand::ClearKeyPressesIntercepts => { clear_key_presses_intercepts(&mut env) }, PluginCommand::ReplacePaneWithExistingPane( pane_id_to_replace, existing_pane_id, suppress_replaced_pane, ) => replace_pane_with_existing_pane( &mut env, pane_id_to_replace.into(), existing_pane_id.into(), suppress_replaced_pane, ), PluginCommand::RunAction(action, context) => run_action(&env, action, context), PluginCommand::GetSessionEnvironmentVariables => { get_session_environment_variables(env); }, PluginCommand::OpenCommandPaneInNewTab(command_to_run, context) => { open_command_pane_in_new_tab(env, command_to_run, context); }, PluginCommand::OpenPluginPaneInNewTab { plugin_url, configuration, context, } => { open_plugin_pane_in_new_tab(env, plugin_url, configuration, context); }, PluginCommand::OpenPluginPaneFloating { plugin_url, configuration, floating_pane_coordinates, context, } => { open_plugin_pane_floating( env, plugin_url, configuration, floating_pane_coordinates, context, ); }, PluginCommand::OpenEditorPaneInNewTab(file_to_open, context) => { open_editor_pane_in_new_tab(env, file_to_open, context); }, PluginCommand::OpenCommandPaneInPlaceOfPaneId( pane_id, command_to_run, close_replaced_pane, context, ) => open_command_pane_in_place_of_pane_id( env, pane_id, command_to_run, close_replaced_pane, context, ), PluginCommand::OpenTerminalPaneInPlaceOfPaneId( pane_id, cwd, close_replaced_pane, ) => open_terminal_pane_in_place_of_pane_id( env, pane_id, cwd, close_replaced_pane, ), PluginCommand::OpenEditPaneInPlaceOfPaneId( pane_id, file_to_open, close_replaced_pane, context, ) => open_edit_pane_in_place_of_pane_id( env, pane_id, file_to_open, close_replaced_pane, context, ), PluginCommand::ShowFloatingPanes { tab_id } => show_floating_panes(env, tab_id), PluginCommand::HideFloatingPanes { tab_id } => hide_floating_panes(env, tab_id), PluginCommand::SetPaneRegexHighlights(pane_id, highlights) => { set_pane_regex_highlights(env, pane_id, highlights) }, PluginCommand::ClearPaneHighlights(pane_id) => { clear_pane_highlights(env, pane_id) }, }, (PermissionStatus::Denied, permission) => { log::error!( "Plugin '{}' permission '{}' denied - Command '{:?}' denied", env.name(), permission .map(|p| p.to_string()) .unwrap_or("UNKNOWN".to_owned()), CommandType::from_str(&command.to_string()).with_context(err_context)? ); }, }; Ok(()) }) .with_context(|| format!("failed to run plugin command {}", env.name())) .non_fatal(); } fn subscribe(env: &PluginEnv, event_list: HashSet) -> Result<()> { env.subscriptions .lock() .to_anyhow()? .extend(event_list.clone()); env.senders .send_to_plugin(PluginInstruction::PluginSubscribedToEvents( env.plugin_id, env.client_id, event_list, )) } fn unblock_cli_pipe_input(env: &PluginEnv, pipe_name: String) { env.input_pipes_to_unblock.lock().unwrap().insert(pipe_name); } fn block_cli_pipe_input(env: &PluginEnv, pipe_name: String) { env.input_pipes_to_block.lock().unwrap().insert(pipe_name); } fn cli_pipe_output(env: &PluginEnv, pipe_name: String, output: String) -> Result<()> { env.senders .send_to_server(ServerInstruction::CliPipeOutput(pipe_name, output)) .context("failed to send pipe output") } fn message_to_plugin(env: &PluginEnv, mut message_to_plugin: MessageToPlugin) -> Result<()> { if message_to_plugin.plugin_url.as_ref().map(|s| s.as_str()) == Some("zellij:OWN_URL") { message_to_plugin.plugin_url = Some(env.plugin.location.display()); } if !message_to_plugin.has_cwd() { message_to_plugin = message_to_plugin.new_plugin_instance_should_have_cwd(env.plugin_cwd.clone()); } if !message_to_plugin.plugin_config.contains_key("caller_cwd") { message_to_plugin.plugin_config.insert( "caller_cwd".to_owned(), env.plugin_cwd.display().to_string(), ); } env.senders .send_to_plugin(PluginInstruction::MessageFromPlugin { source_plugin_id: env.plugin_id, message: message_to_plugin, }) .context("failed to send message to plugin") } fn unsubscribe(env: &PluginEnv, event_list: HashSet) -> Result<()> { env.subscriptions .lock() .to_anyhow()? .retain(|k| !event_list.contains(k)); if event_list.contains(&EventType::PaneRenderReportWithAnsi) { let _ = env .senders .send_to_plugin(PluginInstruction::PluginSubscribedToEvents( env.plugin_id, env.client_id, HashSet::new(), // empty set signals a recheck, not a new subscription )); } Ok(()) } fn set_selectable(env: &PluginEnv, selectable: bool) { env.senders .send_to_screen(ScreenInstruction::SetSelectable( PaneId::Plugin(env.plugin_id), selectable, )) .with_context(|| { format!( "failed to set plugin {} selectable from plugin {}", selectable, env.name() ) }) .non_fatal(); } fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) { env.senders .send_to_screen(ScreenInstruction::ShowPluginCursor( env.plugin_id, env.client_id, cursor_position, )) .with_context(|| { format!( "failed to {} plugin cursor from plugin {}", if cursor_position.is_some() { "show" } else { "hide" }, env.name() ) }) .non_fatal(); } fn request_permission(env: &PluginEnv, permissions: Vec) -> Result<()> { if PermissionCache::from_path_or_default(None) .check_permissions(env.plugin.location.to_string(), &permissions) { return env .senders .send_to_plugin(PluginInstruction::PermissionRequestResult( env.plugin_id, Some(env.client_id), permissions.to_vec(), PermissionStatus::Granted, None, )); } // we do this so that messages that have arrived while the user is seeing the permission screen // will be cached and reapplied once the permission is granted let _ = env .senders .send_to_plugin(PluginInstruction::CachePluginEvents { plugin_id: env.plugin_id, }); env.senders .send_to_screen(ScreenInstruction::RequestPluginPermissions( env.plugin_id, PluginPermission::new(env.plugin.location.to_string(), permissions), )) } fn get_plugin_ids(env: &PluginEnv) { let ids = PluginIds { plugin_id: env.plugin_id, zellij_pid: process::id(), initial_cwd: env.plugin_cwd.clone(), client_id: env.client_id, }; ProtobufPluginIds::try_from(ids) .map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e)) .and_then(|serialized| { wasi_write_object(env, &serialized.encode_to_vec())?; Ok(()) }) .with_context(|| { format!( "failed to query plugin IDs from host for plugin {}", env.name() ) }) .non_fatal(); } fn get_zellij_version(env: &PluginEnv) { let protobuf_zellij_version = ProtobufZellijVersion { version: VERSION.to_owned(), }; wasi_write_object(env, &protobuf_zellij_version.encode_to_vec()) .with_context(|| { format!( "failed to request zellij version from host for plugin {}", env.name() ) }) .non_fatal(); } fn generate_random_name(env: &PluginEnv) { let name = generate_random_name_impl(); let response = ProtobufGenerateRandomNameResponse { name }; wasi_write_object(env, &response.encode_to_vec()) .with_context(|| { format!( "failed to send generated random name to plugin {}", env.name() ) }) .non_fatal(); } fn dump_layout(env: &PluginEnv, layout_name: String) { let layout_path = PathBuf::from(&layout_name); let layout_dir = env.layout_dir.clone().or_else(default_layout_dir); let response = match Layout::stringified_from_dir(&layout_path, layout_dir.as_ref()) { Ok((_, layout_content, _)) => ProtobufDumpLayoutResponse { result: Some(dump_layout_response::Result::LayoutContent(layout_content)), }, Err(e) => ProtobufDumpLayoutResponse { result: Some(dump_layout_response::Result::Error(e.to_string())), }, }; wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to send layout dump to plugin {}", env.name())) .non_fatal(); } fn get_layout_dir(env: &PluginEnv) { // Get layout dir from env or fall back to default let layout_dir = env.layout_dir.clone().or_else(default_layout_dir); // Convert PathBuf to String let layout_dir_string = layout_dir .and_then(|path| path.to_str().map(|s| s.to_string())) .unwrap_or_else(|| String::from("")); let response = ProtobufGetLayoutDirResponse { layout_dir: layout_dir_string, }; wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to send layout dir to plugin {}", env.name())) .non_fatal(); } fn get_session_environment_variables(env: &PluginEnv) { use zellij_utils::plugin_api::plugin_command::EnvVariable as ProtobufEnvVariable; let env_vars: Vec = env .session_env_vars .iter() .map(|(name, value)| ProtobufEnvVariable { name: name.clone(), value: value.clone(), }) .collect(); let response = ProtobufGetSessionEnvironmentVariablesResponse { env_vars }; wasi_write_object(env, &response.encode_to_vec()) .with_context(|| { format!( "failed to send session environment variables to plugin {}", env.name() ) }) .non_fatal(); } fn open_command_pane_in_new_tab( env: &PluginEnv, command_to_run: CommandToRun, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let args = command_to_run.args; let hold_on_close = true; let hold_on_start = false; let run_command_action = RunCommandAction { command, args, cwd, direction: None, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title: false, }; let initial_panes = Some(vec![CommandOrPlugin::Command(run_command_action)]); let action = Action::NewTab { tiled_layout: None, floating_layouts: vec![], swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: None, should_change_focus_to_new_tab: true, cwd: None, initial_panes, first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open command pane in new tab"); let result = apply_action!(action, error_msg, env); let tab_id = result.as_ref().and_then(|r| r.affected_tab_id); let pane_id = result .as_ref() .and_then(|r| r.affected_pane_id) .map(|p| p.into()); let response = ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id }); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_in_new_tab response")) .non_fatal(); } fn open_plugin_pane_in_new_tab( env: &PluginEnv, plugin_url: String, configuration: BTreeMap, context: BTreeMap, ) { let run_plugin_or_alias = RunPluginOrAlias::from_url( &plugin_url, &Some(configuration), None, Some(env.plugin_cwd.clone()), ); let run_plugin_or_alias = match run_plugin_or_alias { Ok(r) => r, Err(e) => { log::error!("Failed to parse plugin url '{}': {}", plugin_url, e); let response = ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse::default()); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| { format!("failed to write open_plugin_pane_in_new_tab error response") }) .non_fatal(); return; }, }; let _ = context; // context is not currently used for plugin panes let initial_panes = Some(vec![CommandOrPlugin::Plugin(run_plugin_or_alias)]); let action = Action::NewTab { tiled_layout: None, floating_layouts: vec![], swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: None, should_change_focus_to_new_tab: true, cwd: None, initial_panes, first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open plugin pane in new tab"); let result = apply_action!(action, error_msg, env); let tab_id = result.as_ref().and_then(|r| r.affected_tab_id); let pane_id = result .as_ref() .and_then(|r| r.affected_pane_id) .map(|p| p.into()); let response = ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id }); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_plugin_pane_in_new_tab response")) .non_fatal(); } fn open_plugin_pane_floating( env: &PluginEnv, plugin_url: String, configuration: BTreeMap, floating_pane_coordinates: Option, context: BTreeMap, ) { let run_plugin_or_alias = RunPluginOrAlias::from_url( &plugin_url, &Some(configuration), None, Some(env.plugin_cwd.clone()), ); let run_plugin_or_alias = match run_plugin_or_alias { Ok(r) => r, Err(e) => { log::error!("Failed to parse plugin url '{}': {}", plugin_url, e); let response = ProtobufOpenPluginPaneFloatingResponse::from( OpenPluginPaneFloatingResponse::default(), ); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| { format!("failed to write open_plugin_pane_floating error response") }) .non_fatal(); return; }, }; let _ = context; // context is not currently used for plugin panes let action = Action::NewFloatingPluginPane { plugin: run_plugin_or_alias, pane_name: None, skip_cache: false, cwd: Some(env.plugin_cwd.clone()), coordinates: floating_pane_coordinates, no_focus: false, tab_id: None, }; let error_msg = || format!("Failed to open floating plugin pane"); let result = apply_action!(action, error_msg, env); let pane_id: OpenPluginPaneFloatingResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); let response = ProtobufOpenPluginPaneFloatingResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_plugin_pane_floating response")) .non_fatal(); } fn open_editor_pane_in_new_tab( env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap, ) { let _ = context; // context is not currently used for editor panes in new tab let path = translate_plugin_path(env, file_to_open.path); let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let file_to_open = FileToOpen { path, cwd, ..file_to_open }; let initial_panes = Some(vec![CommandOrPlugin::File(file_to_open)]); let action = Action::NewTab { tiled_layout: None, floating_layouts: vec![], swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: None, should_change_focus_to_new_tab: true, cwd: None, initial_panes, first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open editor pane in new tab"); let result = apply_action!(action, error_msg, env); let tab_id = result.as_ref().and_then(|r| r.affected_tab_id); let pane_id = result .as_ref() .and_then(|r| r.affected_pane_id) .map(|p| p.into()); let response = ProtobufOpenPaneInNewTabResponse::from(OpenPaneInNewTabResponse { tab_id, pane_id }); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_editor_pane_in_new_tab response")) .non_fatal(); } fn get_focused_pane_info(env: &PluginEnv) { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; let err_context = || { format!( "failed to get focused pane info for client {:?} from plugin {}", env.client_id, env.name() ) }; // Create oneshot channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Send request to screen thread env.senders .send_to_screen(ScreenInstruction::GetFocusedPaneInfo { client_id: env.client_id, response_channel: response_sender, }) .with_context(err_context) .non_fatal(); // Block waiting for response with 100ms timeout let response = match response_receiver.recv_timeout(Duration::from_millis(100)) { Ok(response) => response, Err(RecvTimeoutError::Timeout) => { log::error!( "GetFocusedPaneInfo timed out for plugin {} for client {:?}", env.plugin_id, env.client_id ); GetFocusedPaneInfoResponse::Err("Timeout retrieving focused pane info".to_string()) }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetFocusedPaneInfo channel disconnected for plugin {}", env.plugin_id ); GetFocusedPaneInfoResponse::Err( "Channel disconnected while retrieving focused pane info".to_string(), ) }, }; // Convert to protobuf and write response back to plugin let protobuf_response = ProtobufGetFocusedPaneInfoResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(err_context) .non_fatal(); } fn parse_layout(env: &PluginEnv, layout_string: String) { use std::time::{SystemTime, UNIX_EPOCH}; // Parse the KDL layout - same parameters as list_available_layouts in layout.rs:1260 let parse_result = Layout::from_kdl( &layout_string, Some("parse_layout_api".to_string()), // file_name for error reporting None, // no swap layouts None, // no cwd ); let response = match parse_result { Ok(layout) => { // Extract tabs from layout // This logic matches LayoutMetadata::from in data.rs:1762-1772 let layout_tabs = layout.tabs(); let tabs = if layout_tabs.is_empty() { // Use default template if no explicit tabs defined let (tiled_pane_layout, floating_pane_layout) = layout.new_tab(); vec![TabMetadata::from(&( None, tiled_pane_layout, floating_pane_layout, ))] } else { layout .tabs() .into_iter() .map(|tab| TabMetadata::from(&tab)) .collect() }; // Get current time as Unix epoch timestamp let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() .to_string(); let layout_metadata = LayoutMetadata { tabs, creation_time: current_time.clone(), update_time: current_time, }; // Convert LayoutMetadata to protobuf match layout_metadata.try_into() { Ok(protobuf_metadata) => ProtobufParseLayoutResponse { result: Some(parse_layout_response::Result::Metadata(protobuf_metadata)), }, Err(e) => { log::error!("Failed to convert metadata to protobuf: {}", e); // Fallback to SyntaxError if conversion fails let error = LayoutParsingError::SyntaxError; let protobuf_error = error .try_into() .unwrap_or_else(|_| ProtobufLayoutParsingError { error_type: Some(ProtobufLayoutParsingErrorType::SyntaxError( ProtobufSyntaxError {}, )), }); ProtobufParseLayoutResponse { result: Some(parse_layout_response::Result::Error(protobuf_error)), } }, } }, Err(config_error) => { // Build LayoutParsingError following list_available_layouts pattern // See zellij-utils/src/input/layout.rs:1238-1245 let file_name = "parse_layout_api".to_string(); let source_code = layout_string; let error = match config_error { ConfigError::KdlError(kdl_err) => LayoutParsingError::KdlError { kdl_error: kdl_err, file_name, source_code, }, _ => LayoutParsingError::SyntaxError, }; // Convert LayoutParsingError to protobuf // TryFrom is implemented in zellij-utils/src/plugin_api/event.rs:1293-1314 let protobuf_error = error.try_into().unwrap_or_else(|e| { log::error!("Failed to convert error to protobuf: {}", e); ProtobufLayoutParsingError { error_type: Some(ProtobufLayoutParsingErrorType::SyntaxError( ProtobufSyntaxError {}, )), } }); ProtobufParseLayoutResponse { result: Some(parse_layout_response::Result::Error(protobuf_error)), } }, }; wasi_write_object(env, &response.encode_to_vec()) .with_context(|| { format!( "failed to send parse layout response to plugin {}", env.name() ) }) .non_fatal(); } fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let floating = false; let in_place = false; let start_suppressed = false; let path = translate_plugin_path(env, file_to_open.path); let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let action = Action::EditFile { payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ), direction: None, floating, in_place, close_replaced_pane: false, start_suppressed, coordinates: None, near_current_pane: false, no_focus: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenFileResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file response")) .non_fatal(); } fn run_action(env: &PluginEnv, mut action: Action, context: BTreeMap) { // Clone the necessary data to move into the thread action.populate_originating_plugin(OriginatingPlugin::new( env.plugin_id, env.client_id, context.clone(), )); let action_clone = action.clone(); let client_id = env.client_id; let plugin_id = env.plugin_id; let senders = env.senders.clone(); let default_shell = env.default_shell.clone(); let default_mode = env.default_mode.clone(); let plugin_name = env.name().to_string(); // Spawn a new thread to execute the action thread::spawn(move || { // Execute the action and capture the result let pane_id = match route_action( action, client_id, None, Some(PaneId::Plugin(plugin_id)), senders.clone(), default_shell, None, default_mode, None, ) { Ok((_should_break, result)) => { // Extract pane_id from ActionCompletionResult result.and_then(|r| r.affected_pane_id) }, Err(e) => { log::error!("failed to run action in plugin {}: {:?}", plugin_name, e); None }, }; // After action completes, send ActionComplete event with context // Convert server PaneId to zellij_utils PaneId let pane_id_for_event = pane_id.map(|p| p.into()); let updates = vec![( Some(plugin_id), Some(client_id), Event::ActionComplete(action_clone, pane_id_for_event, context), )]; // Send the ActionComplete event back to the plugin if let Err(e) = senders.send_to_plugin(PluginInstruction::Update(updates)) { log::error!("Failed to send ActionComplete event: {:?}", e); } }); } fn open_file_floating( env: &PluginEnv, file_to_open: FileToOpen, floating_pane_coordinates: Option, context: BTreeMap, ) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let floating = true; let in_place = false; let start_suppressed = false; let path = translate_plugin_path(env, file_to_open.path); let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let action = Action::EditFile { payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ), direction: None, floating, in_place, close_replaced_pane: false, start_suppressed, coordinates: floating_pane_coordinates, near_current_pane: false, no_focus: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenFileFloatingResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileFloatingResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file_floating response")) .non_fatal(); } fn open_file_in_place( env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap, ) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let floating = false; let in_place = true; let start_suppressed = false; let path = translate_plugin_path(env, file_to_open.path); let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let action = Action::EditFile { payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ), direction: None, floating, in_place, close_replaced_pane: false, start_suppressed, coordinates: None, near_current_pane: false, no_focus: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenFileInPlaceResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileInPlaceResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file_in_place response")) .non_fatal(); } fn open_file_near_plugin( env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap, ) { let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let path = translate_plugin_path(env, file_to_open.path); let open_file_payload = OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ); let title = format!("Editing: {}", open_file_payload.path.display()); let start_suppressed = false; let open_file = TerminalAction::OpenFile(open_file_payload); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let pty_instr = PtyInstruction::SpawnTerminal( Some(open_file), Some(title), NewPanePlacement::default(), start_suppressed, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose ); let _ = env.senders.send_to_pty(pty_instr); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_file_near_plugin", false); let pane_id: OpenFileNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file_near_plugin response")) .non_fatal(); } fn open_file_floating_near_plugin( env: &PluginEnv, file_to_open: FileToOpen, floating_pane_coordinates: Option, context: BTreeMap, ) { let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let path = translate_plugin_path(env, file_to_open.path); let open_file_payload = OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ); let title = format!("Editing: {}", open_file_payload.path.display()); let start_suppressed = false; let open_file = TerminalAction::OpenFile(open_file_payload); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let pty_instr = PtyInstruction::SpawnTerminal( Some(open_file), Some(title), NewPanePlacement::Floating(floating_pane_coordinates), start_suppressed, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose ); let _ = env.senders.send_to_pty(pty_instr); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_file_floating_near_plugin", false); let pane_id: OpenFileFloatingNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileFloatingNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file_floating_near_plugin response")) .non_fatal(); } fn open_file_in_place_of_plugin( env: &PluginEnv, file_to_open: FileToOpen, close_plugin_after_replace: bool, context: BTreeMap, ) { let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let path = translate_plugin_path(env, file_to_open.path); let open_file_payload = OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ); let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let pty_instr = PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), close_plugin_after_replace, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), ); let _ = env.senders.send_to_pty(pty_instr); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_file_in_place_of_plugin", false); let pane_id: OpenFileInPlaceOfPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenFileInPlaceOfPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_file_in_place_of_plugin response")) .non_fatal(); } fn open_terminal(env: &PluginEnv, cwd: PathBuf) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd); let run_command_action: Option = match default_shell { TerminalAction::RunCommand(run_command) => Some(run_command.into()), _ => None, }; let action = Action::NewTiledPane { purpose: None, direction: None, command: run_command_action, pane_name: None, near_current_pane: false, no_focus: false, borderless: None, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenTerminalResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal response")) .non_fatal(); } fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) { let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); let name = None; default_shell.change_cwd(cwd); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal( Some(default_shell), name, NewPanePlacement::Tiled { direction: None, borderless: None, }, false, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_terminal_near_plugin", false); let pane_id: OpenTerminalNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_near_plugin response")) .non_fatal(); } fn open_terminal_floating( env: &PluginEnv, cwd: PathBuf, floating_pane_coordinates: Option, ) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd); let run_command_action: Option = match default_shell { TerminalAction::RunCommand(run_command) => Some(run_command.into()), _ => None, }; let action = Action::NewFloatingPane { purpose: None, command: run_command_action, pane_name: None, coordinates: floating_pane_coordinates, near_current_pane: false, no_focus: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenTerminalFloatingResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalFloatingResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_floating response")) .non_fatal(); } fn open_terminal_floating_near_plugin( env: &PluginEnv, cwd: PathBuf, floating_pane_coordinates: Option, ) { let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd); let name = None; // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal( Some(default_shell), name, NewPanePlacement::Floating(floating_pane_coordinates), false, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_terminal_floating_near_plugin", false); let pane_id: OpenTerminalFloatingNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalFloatingNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_floating_near_plugin response")) .non_fatal(); } fn open_terminal_in_place(env: &PluginEnv, cwd: PathBuf) { let error_msg = || format!("failed to open file in plugin {}", env.name()); let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd); let run_command_action: Option = match default_shell { TerminalAction::RunCommand(run_command) => Some(run_command.into()), _ => None, }; let action = Action::NewInPlacePane { command: run_command_action, pane_name: None, near_current_pane: false, no_focus: false, pane_id_to_replace: None, close_replaced_pane: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenTerminalInPlaceResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalInPlaceResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_in_place response")) .non_fatal(); } fn open_terminal_in_place_of_plugin( env: &PluginEnv, cwd: PathBuf, close_plugin_after_replace: bool, ) { let cwd = translate_plugin_path(env, cwd); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd); let name = None; // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env .senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( Some(default_shell), name, close_plugin_after_replace, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_terminal_in_place_of_plugin", false); let pane_id: OpenTerminalInPlaceOfPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenTerminalInPlaceOfPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_in_place_of_plugin response")) .non_fatal(); } fn open_command_pane_in_place_of_plugin( env: &PluginEnv, command_to_run: CommandToRun, close_plugin_after_replace: bool, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let run_cmd = TerminalAction::RunCommand(run_command_action.into()); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env .senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( Some(run_cmd), name, close_plugin_after_replace, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_command_pane_in_place_of_plugin", false); let pane_id: OpenCommandPaneInPlaceOfPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneInPlaceOfPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_in_place_of_plugin response")) .non_fatal(); } fn open_terminal_pane_in_place_of_pane_id( env: &PluginEnv, pane_id_to_replace: zellij_utils::data::PaneId, cwd: FileToOpen, close_replaced_pane: bool, ) { let cwd_path = translate_plugin_path(env, cwd.path); let mut default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); default_shell.change_cwd(cwd_path); let name = None; let (completion_tx, completion_rx) = oneshot::channel(); let _ = env .senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( Some(default_shell), name, close_replaced_pane, ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()), Some(NotificationEnd::new(completion_tx)), )); let result = wait_for_action_completion( completion_rx, "open_terminal_pane_in_place_of_pane_id", false, ); let pane_id: OpenTerminalPaneInPlaceOfPaneIdResponse = result.affected_pane_id.map(|p| p.into()); let response = ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_terminal_pane_in_place_of_pane_id response")) .non_fatal(); } fn open_command_pane_in_place_of_pane_id( env: &PluginEnv, pane_id_to_replace: zellij_utils::data::PaneId, command_to_run: CommandToRun, close_replaced_pane: bool, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let run_cmd = TerminalAction::RunCommand(run_command_action.into()); let (completion_tx, completion_rx) = oneshot::channel(); let _ = env .senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( Some(run_cmd), name, close_replaced_pane, ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()), Some(NotificationEnd::new(completion_tx)), )); let result = wait_for_action_completion( completion_rx, "open_command_pane_in_place_of_pane_id", false, ); let pane_id: OpenCommandPaneInPlaceOfPaneIdResponse = result.affected_pane_id.map(|p| p.into()); let response = ProtobufOpenCommandPaneInPlaceOfPaneIdResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_in_place_of_pane_id response")) .non_fatal(); } fn open_edit_pane_in_place_of_pane_id( env: &PluginEnv, pane_id_to_replace: zellij_utils::data::PaneId, file_to_open: FileToOpen, close_replaced_pane: bool, context: BTreeMap, ) { let cwd = file_to_open .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let path = translate_plugin_path(env, file_to_open.path); let open_file_payload = OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin( OriginatingPlugin::new(env.plugin_id, env.client_id, context), ); let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); let (completion_tx, completion_rx) = oneshot::channel(); let pty_instr = PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), close_replaced_pane, ClientTabIndexOrPaneId::PaneId(pane_id_to_replace.into()), Some(NotificationEnd::new(completion_tx)), ); let _ = env.senders.send_to_pty(pty_instr); let result = wait_for_action_completion(completion_rx, "open_edit_pane_in_place_of_pane_id", false); let pane_id: OpenEditPaneInPlaceOfPaneIdResponse = result.affected_pane_id.map(|p| p.into()); let response = ProtobufOpenEditPaneInPlaceOfPaneIdResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_edit_pane_in_place_of_pane_id response")) .non_fatal(); } fn open_command_pane( env: &PluginEnv, command_to_run: CommandToRun, context: BTreeMap, ) { let error_msg = || format!("failed to open command in plugin {}", env.name()); let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let action = Action::NewTiledPane { purpose: None, direction, command: Some(run_command_action), pane_name: name, near_current_pane: false, no_focus: false, borderless: None, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenCommandPaneResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane response")) .non_fatal(); } fn open_command_pane_near_plugin( env: &PluginEnv, command_to_run: CommandToRun, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let run_cmd = TerminalAction::RunCommand(run_command_action.into()); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal( Some(run_cmd), name, NewPanePlacement::Tiled { direction: None, borderless: None, }, false, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_command_pane_near_plugin", false); let pane_id: OpenCommandPaneNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_near_plugin response")) .non_fatal(); } fn open_command_pane_floating( env: &PluginEnv, command_to_run: CommandToRun, floating_pane_coordinates: Option, context: BTreeMap, ) { let error_msg = || format!("failed to open command in plugin {}", env.name()); let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let action = Action::NewFloatingPane { purpose: None, command: Some(run_command_action), pane_name: name, coordinates: floating_pane_coordinates, near_current_pane: false, no_focus: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenCommandPaneFloatingResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneFloatingResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_floating response")) .non_fatal(); } fn open_command_pane_floating_near_plugin( env: &PluginEnv, command_to_run: CommandToRun, floating_pane_coordinates: Option, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let run_cmd = TerminalAction::RunCommand(run_command_action.into()); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal( Some(run_cmd), name, NewPanePlacement::Floating(floating_pane_coordinates), false, ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose )); // Wait for completion let result = wait_for_action_completion( completion_rx, "open_command_pane_floating_near_plugin", false, ); let pane_id: OpenCommandPaneFloatingNearPluginResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneFloatingNearPluginResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_floating_near_plugin response")) .non_fatal(); } fn open_command_pane_in_place( env: &PluginEnv, command_to_run: CommandToRun, context: BTreeMap, ) { let error_msg = || format!("failed to open command in plugin {}", env.name()); let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let action = Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, near_current_pane: false, no_focus: false, pane_id_to_replace: None, close_replaced_pane: false, tab_id: None, }; let result = apply_action!(action, error_msg, env); // Extract pane_id from result and convert to zellij_utils PaneId let pane_id: OpenCommandPaneInPlaceResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneInPlaceResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_in_place response")) .non_fatal(); } fn open_command_pane_background( env: &PluginEnv, command_to_run: CommandToRun, context: BTreeMap, ) { let command = command_to_run.path; let cwd = command_to_run .cwd .map(|cwd| translate_plugin_path(env, cwd)) .or_else(|| Some(env.plugin_cwd.clone())); let args = command_to_run.args; let direction = None; let hold_on_close = true; let hold_on_start = false; let start_suppressed = true; let name = None; let use_terminal_title = false; // TODO: support this let run_command_action = RunCommandAction { command, args, cwd, direction, hold_on_close, hold_on_start, originating_plugin: Some(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), use_terminal_title, }; let run_cmd = TerminalAction::RunCommand(run_command_action.into()); // Create completion channel let (completion_tx, completion_rx) = oneshot::channel(); let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal( Some(run_cmd), name, NewPanePlacement::default(), start_suppressed, ClientTabIndexOrPaneId::ClientId(env.client_id), Some(NotificationEnd::new(completion_tx)), false, // set_blocking None, // purpose )); // Wait for completion let result = wait_for_action_completion(completion_rx, "open_command_pane_background", false); let pane_id: OpenCommandPaneBackgroundResponse = result.affected_pane_id.map(|p| p.into()); // Write response to plugin let response = ProtobufOpenCommandPaneBackgroundResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write open_command_pane_background response")) .non_fatal(); } fn rerun_command_pane(env: &PluginEnv, terminal_pane_id: u32) { let _ = env .senders .send_to_screen(ScreenInstruction::RerunCommandPane(terminal_pane_id, None)); } fn switch_tab_to(env: &PluginEnv, tab_idx: u32) { env.senders .send_to_screen(ScreenInstruction::GoToTab( tab_idx, Some(env.client_id), None, )) .with_context(|| { format!( "failed to switch to tab {tab_idx} from plugin {}", env.name() ) }) .non_fatal(); } fn set_timeout(env: &PluginEnv, secs: f64) { let send_plugin_instructions = env.senders.to_plugin.clone(); let update_target = Some(env.plugin_id); let client_id = env.client_id; let plugin_name = env.name(); // Use tokio runtime for async I/O (timer operation) get_tokio_runtime().spawn(async move { let start_time = Instant::now(); tokio::time::sleep(Duration::from_secs_f64(secs)).await; // FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the // time it takes an event to actually reach the plugin after it's sent to the `wasm` thread. let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); send_plugin_instructions .ok_or(anyhow!("found no sender to send plugin instruction to")) .and_then(|sender| { sender .send(PluginInstruction::Update(vec![( update_target, Some(client_id), Event::Timer(elapsed_time), )])) .to_anyhow() }) .with_context(|| { format!( "failed to set host timeout of {secs} s for plugin {}", plugin_name ) }) .non_fatal(); }); } fn exec_cmd(env: &PluginEnv, mut command_line: Vec) { log::warn!("The ExecCmd plugin command is deprecated and will be removed in a future version. Please use RunCmd instead (it has all the things and can even show you STDOUT/STDERR and an exit code!)"); let err_context = || { format!( "failed to execute command on host for plugin '{}'", env.name() ) }; let command = command_line.remove(0); // Bail out if we're forbidden to run command if !env.plugin._allow_exec_host_cmd { warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", cmd = command, args = command_line.join(" ")); return; } // Here, we don't wait the command to finish process::Command::new(command) .args(command_line) .spawn() .with_context(err_context) .non_fatal(); } fn run_command( env: &PluginEnv, mut command_line: Vec, env_variables: BTreeMap, cwd: PathBuf, context: BTreeMap, ) { if command_line.is_empty() { log::error!("Command cannot be empty"); } else { let command = command_line.remove(0); let cwd = translate_plugin_path(env, cwd); let _ = env .senders .send_to_background_jobs(BackgroundJob::RunCommand( env.plugin_id, env.client_id, command, command_line, env_variables, cwd, context, )); } } fn web_request( env: &PluginEnv, url: String, verb: HttpVerb, headers: BTreeMap, body: Vec, context: BTreeMap, ) { let _ = env .senders .send_to_background_jobs(BackgroundJob::WebRequest( env.plugin_id, env.client_id, url, verb, headers, body, context, )); } fn post_message_to(env: &PluginEnv, plugin_message: PluginMessage) -> Result<()> { let worker_name = plugin_message .worker_name .ok_or(anyhow!("Worker name not specified in message to worker"))?; env.senders .send_to_plugin(PluginInstruction::PostMessagesToPluginWorker( env.plugin_id, env.client_id, worker_name, vec![(plugin_message.name, plugin_message.payload)], )) } fn post_message_to_plugin(env: &PluginEnv, plugin_message: PluginMessage) -> Result<()> { if let Some(worker_name) = plugin_message.worker_name { return Err(anyhow!( "Worker name (\"{}\") should not be specified in message to plugin", worker_name )); } env.senders .send_to_plugin(PluginInstruction::PostMessageToPlugin( env.plugin_id, env.client_id, plugin_message.name, plugin_message.payload, )) } fn hide_self(env: &PluginEnv) -> Result<()> { env.senders .send_to_screen(ScreenInstruction::SuppressPane( PaneId::Plugin(env.plugin_id), env.client_id, )) .with_context(|| format!("failed to hide self")) } fn hide_pane_with_id(env: &PluginEnv, pane_id: PaneId) -> Result<()> { env.senders .send_to_screen(ScreenInstruction::SuppressPane(pane_id, env.client_id)) .with_context(|| format!("failed to hide self")) } fn show_self(env: &PluginEnv, should_float_if_hidden: bool) { let action = Action::FocusPluginPaneWithId { pane_id: env.plugin_id, should_float_if_hidden, should_be_in_place_if_hidden: false, }; let error_msg = || format!("Failed to show self for plugin"); apply_action!(action, error_msg, env); } fn show_pane_with_id( env: &PluginEnv, pane_id: PaneId, should_float_if_hidden: bool, should_focus_pane: bool, ) { if should_focus_pane { let _ = env .senders .send_to_screen(ScreenInstruction::FocusPaneWithId( pane_id, should_float_if_hidden, false, env.client_id, None, )); } else { let _ = env .senders .send_to_screen(ScreenInstruction::UnsuppressOrExpandPane( pane_id, should_float_if_hidden, )); } } fn close_self(env: &PluginEnv) { env.senders .send_to_screen(ScreenInstruction::ClosePane( PaneId::Plugin(env.plugin_id), None, None, None, )) .with_context(|| format!("failed to close self")) .non_fatal(); env.senders .send_to_plugin(PluginInstruction::Unload(env.plugin_id)) .with_context(|| format!("failed to close self")) .non_fatal(); } fn reconfigure(env: &PluginEnv, new_config: String, write_config_to_disk: bool) -> Result<()> { let err_context = || "Failed to reconfigure"; let client_id = env.client_id; env.senders .send_to_server(ServerInstruction::Reconfigure { client_id, config: new_config, write_config_to_disk, }) .with_context(err_context)?; Ok(()) } fn rebind_keys( env: &PluginEnv, keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, write_config_to_disk: bool, ) -> Result<()> { let err_context = || "Failed to rebind_keys"; let client_id = env.client_id; env.senders .send_to_server(ServerInstruction::RebindKeys { client_id, keys_to_rebind, keys_to_unbind, write_config_to_disk, }) .with_context(err_context)?; Ok(()) } fn switch_to_mode(env: &PluginEnv, input_mode: InputMode) { let action = Action::SwitchToMode { input_mode }; let error_msg = || format!("failed to switch to mode in plugin {}", env.name()); apply_action!(action, error_msg, env); } fn new_tabs_with_layout(env: &PluginEnv, raw_layout: &str) -> Result<()> { // TODO: cwd let layout = Layout::from_str( &raw_layout, format!("Layout from plugin: {}", env.name()), None, None, ) .map_err(|e| anyhow!("Failed to parse layout: {:?}", e))?; apply_layout(env, layout); Ok(()) } fn new_tabs_with_layout_info(env: &PluginEnv, layout_info: LayoutInfo) -> Result<()> { // TODO: cwd let layout = Layout::from_layout_info(&env.layout_dir, layout_info) .map_err(|e| anyhow!("Failed to parse layout: {:?}", e))?; apply_layout(env, layout); Ok(()) } fn apply_layout(env: &PluginEnv, layout: Layout) { let mut tabs_to_open = vec![]; let tabs = layout.tabs(); let cwd = None; // TODO: add this to the plugin API if tabs.is_empty() { let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); let action = Action::NewTab { tiled_layout: layout.template.as_ref().map(|t| t.0.clone()), floating_layouts: layout.template.map(|t| t.1).unwrap_or_default(), swap_tiled_layouts, swap_floating_layouts, tab_name: None, should_change_focus_to_new_tab: true, cwd, initial_panes: None, first_pane_unblock_condition: None, }; tabs_to_open.push(action); } else { let focused_tab_index = layout.focused_tab_index().unwrap_or(0); for (tab_index, (tab_name, tiled_pane_layout, floating_pane_layout)) in layout.tabs().into_iter().enumerate() { let should_focus_tab = tab_index == focused_tab_index; let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); let action = Action::NewTab { tiled_layout: Some(tiled_pane_layout), floating_layouts: floating_pane_layout, swap_tiled_layouts, swap_floating_layouts, tab_name, should_change_focus_to_new_tab: should_focus_tab, cwd: cwd.clone(), initial_panes: None, first_pane_unblock_condition: None, }; tabs_to_open.push(action); } } let mut tab_ids = Vec::new(); for action in tabs_to_open { let error_msg = || format!("Failed to create layout tab"); let result = apply_action!(action, error_msg, env); // Collect tab ID from each action if let Some(tab_id) = result.and_then(|r| r.affected_tab_id) { tab_ids.push(tab_id); } } // Write response let response = ProtobufNewTabsResponse::from(tab_ids); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write layout tabs response")) .non_fatal(); } fn new_tab(env: &PluginEnv, name: Option, cwd: Option) { let cwd = cwd.map(|c| translate_plugin_path(env, PathBuf::from(c))); let action = Action::NewTab { tiled_layout: None, floating_layouts: vec![], swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: name, should_change_focus_to_new_tab: true, cwd, initial_panes: None, first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open new tab"); let result = apply_action!(action, error_msg, env); // Extract tab_id from result - return None if not present let tab_id: NewTabResponse = result.and_then(|r| r.affected_tab_id); // Write response to plugin let response = ProtobufNewTabResponse::from(tab_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write new_tab response")) .non_fatal(); } fn new_tab_unfocused(env: &PluginEnv, name: Option, cwd: Option) { let cwd = cwd.map(|c| translate_plugin_path(env, PathBuf::from(c))); let action = Action::NewTab { tiled_layout: None, floating_layouts: vec![], swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: name, should_change_focus_to_new_tab: false, cwd, initial_panes: None, first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open new tab (unfocused)"); let result = apply_action!(action, error_msg, env); let tab_id: NewTabUnfocusedResponse = result.and_then(|r| r.affected_tab_id); let response = ProtobufNewTabUnfocusedResponse::from(tab_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write new_tab_unfocused response")) .non_fatal(); } fn new_tiled_pane_in_tab(env: &PluginEnv, tab_position: usize) { let error_msg = || format!("failed to open new tiled pane in tab {}", tab_position); let default_shell = env.default_shell.clone().unwrap_or_else(|| { TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() }) }); let run_command_action: Option = match default_shell { TerminalAction::RunCommand(run_command) => Some(run_command.into()), _ => None, }; let action = Action::NewTiledPane { purpose: None, direction: None, command: run_command_action, pane_name: None, near_current_pane: false, no_focus: false, borderless: None, tab_id: Some(tab_position), }; let result = apply_action!(action, error_msg, env); let pane_id: NewTiledPaneInTabResponse = result.and_then(|r| r.affected_pane_id).map(|p| p.into()); let response = ProtobufNewTiledPaneInTabResponse::from(pane_id); wasi_write_object(env, &response.encode_to_vec()) .with_context(|| format!("failed to write new_tiled_pane_in_tab response")) .non_fatal(); } fn go_to_next_tab(env: &PluginEnv) { let action = Action::GoToNextTab; let error_msg = || format!("Failed to go to next tab"); apply_action!(action, error_msg, env); } fn go_to_previous_tab(env: &PluginEnv) { let action = Action::GoToPreviousTab; let error_msg = || format!("Failed to go to previous tab"); apply_action!(action, error_msg, env); } fn resize(env: &PluginEnv, resize: Resize) { let error_msg = || format!("failed to resize in plugin {}", env.name()); let action = Action::Resize { resize, direction: None, }; apply_action!(action, error_msg, env); } fn resize_with_direction(env: &PluginEnv, resize: ResizeStrategy) { let error_msg = || format!("failed to resize in plugin {}", env.name()); let action = Action::Resize { resize: resize.resize, direction: resize.direction, }; apply_action!(action, error_msg, env); } fn focus_next_pane(env: &PluginEnv) { let action = Action::FocusNextPane; let error_msg = || format!("Failed to focus next pane"); apply_action!(action, error_msg, env); } fn focus_previous_pane(env: &PluginEnv) { let action = Action::FocusPreviousPane; let error_msg = || format!("Failed to focus previous pane"); apply_action!(action, error_msg, env); } fn focus_last_pane(env: &PluginEnv) { let action = Action::FocusLastPane; let error_msg = || format!("Failed to focus last pane"); apply_action!(action, error_msg, env); } fn move_focus(env: &PluginEnv, direction: Direction) { let error_msg = || format!("failed to move focus in plugin {}", env.name()); let action = Action::MoveFocus { direction }; apply_action!(action, error_msg, env); } fn move_focus_or_tab(env: &PluginEnv, direction: Direction) { let error_msg = || format!("failed to move focus in plugin {}", env.name()); let action = Action::MoveFocusOrTab { direction }; apply_action!(action, error_msg, env); } fn detach(env: &PluginEnv) { let action = Action::Detach; let error_msg = || format!("Failed to detach"); apply_action!(action, error_msg, env); } fn switch_session( env: &PluginEnv, session_name: Option, tab_position: Option, pane_id: Option<(u32, bool)>, layout: Option, cwd: Option, ) -> Result<()> { // pane_id is (id, is_plugin) let err_context = || format!("Failed to switch session"); if let Some(LayoutInfo::Stringified(stringified_layout)) = layout.as_ref() { // we verify the stringified layout here to fail early rather than when parsing it at the // session-switching phase if let Err(e) = Layout::from_kdl(&stringified_layout, None, None, None) { return Err(anyhow!("Failed to deserialize layout: {}", e)); } } let (completion_tx, completion_rx) = oneshot::channel(); if session_name .as_ref() .map(|s| s.contains('/')) .unwrap_or(false) { log::error!("Session names cannot contain \'/\'"); } else { let client_id = env.client_id; let tab_position = tab_position.map(|p| p + 1); // ¯\_()_/¯ let cwd = cwd .map(|c| translate_plugin_path(env, c)) .or_else(|| Some(env.plugin_cwd.clone())); let connect_to_session = ConnectToSession { name: session_name, tab_position, pane_id, layout, cwd, }; env.senders .send_to_server(ServerInstruction::SwitchSession( connect_to_session, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; let wait_forever = false; let _ = wait_for_action_completion(completion_rx, "switch_session", wait_forever); } Ok(()) } fn delete_dead_session(session_name: String) -> Result<()> { std::fs::remove_dir_all(&*ZELLIJ_SESSION_INFO_CACHE_DIR.join(&session_name)) .with_context(|| format!("Failed to delete dead session: {:?}", &session_name)) } fn delete_all_dead_sessions() -> Result<()> { use zellij_utils::consts::is_ipc_socket; let mut live_sessions = vec![]; if let Ok(files) = std::fs::read_dir(&*ZELLIJ_SOCK_DIR) { files.for_each(|file| { if let Ok(file) = file { if let Ok(file_name) = file.file_name().into_string() { if is_ipc_socket(&file.file_type().unwrap()) { live_sessions.push(file_name); } } } }); } let dead_sessions: Vec = match std::fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) { Ok(files_in_session_info_folder) => { let files_that_are_folders = files_in_session_info_folder .filter_map(|f| f.ok().map(|f| f.path())) .filter(|f| f.is_dir()); files_that_are_folders .filter_map(|folder_name| { let session_name = folder_name.file_name()?.to_str()?.to_owned(); if live_sessions.contains(&session_name) { // this is not a dead session... return None; } Some(session_name) }) .collect() }, Err(e) => { log::error!("Failed to read session info cache dir: {:?}", e); vec![] }, }; for session in dead_sessions { delete_dead_session(session)?; } Ok(()) } fn edit_scrollback(env: &PluginEnv) { let action = Action::EditScrollback { ansi: false }; let error_msg = || format!("Failed to edit scrollback"); apply_action!(action, error_msg, env); } fn write(env: &PluginEnv, bytes: Vec) { let error_msg = || format!("failed to write in plugin {}", env.name()); let action = Action::Write { key_with_modifier: None, bytes, is_kitty_keyboard_protocol: false, }; apply_action!(action, error_msg, env); } fn write_chars(env: &PluginEnv, chars_to_write: String) { let error_msg = || format!("failed to write in plugin {}", env.name()); let action = Action::WriteChars { chars: chars_to_write, }; apply_action!(action, error_msg, env); } fn copy_to_clipboard(env: &PluginEnv, text: String) -> Result<()> { env.senders .send_to_screen(ScreenInstruction::CopyTextToClipboard(text, env.plugin_id)) .with_context(|| format!("failed to copy to clipboard in plugin {}", env.name())) } fn toggle_tab(env: &PluginEnv) { let error_msg = || format!("Failed to toggle tab"); let action = Action::ToggleTab; apply_action!(action, error_msg, env); } fn move_pane(env: &PluginEnv) { let error_msg = || format!("failed to move pane in plugin {}", env.name()); let action = Action::MovePane { direction: None }; apply_action!(action, error_msg, env); } fn move_pane_with_direction(env: &PluginEnv, direction: Direction) { let error_msg = || format!("failed to move pane in plugin {}", env.name()); let action = Action::MovePane { direction: Some(direction), }; apply_action!(action, error_msg, env); } fn clear_screen(env: &PluginEnv) { let error_msg = || format!("failed to clear screen in plugin {}", env.name()); let action = Action::ClearScreen; apply_action!(action, error_msg, env); } fn scroll_up(env: &PluginEnv) { let error_msg = || format!("failed to scroll up in plugin {}", env.name()); let action = Action::ScrollUp; apply_action!(action, error_msg, env); } fn scroll_down(env: &PluginEnv) { let error_msg = || format!("failed to scroll down in plugin {}", env.name()); let action = Action::ScrollDown; apply_action!(action, error_msg, env); } fn scroll_to_top(env: &PluginEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.name()); let action = Action::ScrollToTop; apply_action!(action, error_msg, env); } fn scroll_to_bottom(env: &PluginEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.name()); let action = Action::ScrollToBottom; apply_action!(action, error_msg, env); } fn page_scroll_up(env: &PluginEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.name()); let action = Action::PageScrollUp; apply_action!(action, error_msg, env); } fn page_scroll_down(env: &PluginEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.name()); let action = Action::PageScrollDown; apply_action!(action, error_msg, env); } fn toggle_focus_fullscreen(env: &PluginEnv) { let error_msg = || format!("failed to toggle full screen in plugin {}", env.name()); let action = Action::ToggleFocusFullscreen; apply_action!(action, error_msg, env); } fn toggle_focus_no_ui_fullscreen(env: &PluginEnv) { let error_msg = || { format!( "failed to toggle no-ui full screen in plugin {}", env.name() ) }; let action = Action::ToggleFocusNoUiFullscreen; apply_action!(action, error_msg, env); } fn focus_host_session(env: &PluginEnv) { env.senders .send_to_screen(ScreenInstruction::FocusHostSession(env.client_id, None)) .with_context(|| format!("failed to focus host session from plugin {}", env.name())) .non_fatal(); } fn toggle_pane_frames(env: &PluginEnv) { let error_msg = || format!("failed to toggle full screen in plugin {}", env.name()); let action = Action::TogglePaneFrames; apply_action!(action, error_msg, env); } fn set_pane_frame_style(env: &PluginEnv, pane_frame_style: PaneFrameStyle) { let error_msg = || format!("failed to set pane frame style in plugin {}", env.name()); let action = Action::SetPaneFrameStyle(pane_frame_style); apply_action!(action, error_msg, env); } fn toggle_pane_embed_or_eject(env: &PluginEnv) { let error_msg = || { format!( "failed to toggle pane embed or eject in plugin {}", env.name() ) }; let action = Action::TogglePaneEmbedOrFloating; apply_action!(action, error_msg, env); } fn toggle_floating_panes(env: &PluginEnv, tab_id: Option) { let error_msg = || format!("failed to toggle floating panes in plugin {}", env.name()); let action = match tab_id { Some(id) => Action::ToggleFloatingPanesByTabId { id }, None => Action::ToggleFloatingPanes, }; apply_action!(action, error_msg, env); } fn new_pane(env: &PluginEnv) { let error_msg = || format!("failed to open new pane in plugin {}", env.name()); let action = Action::NewPane { direction: None, pane_name: None, start_suppressed: false, }; apply_action!(action, error_msg, env); } fn undo_rename_pane(env: &PluginEnv) { let error_msg = || format!("failed to undo rename pane in plugin {}", env.name()); let action = Action::UndoRenamePane; apply_action!(action, error_msg, env); } fn close_focus(env: &PluginEnv) { let error_msg = || format!("failed to close focused pane in plugin {}", env.name()); let action = Action::CloseFocus; apply_action!(action, error_msg, env); } fn toggle_active_tab_sync(env: &PluginEnv) { let error_msg = || format!("failed to toggle active tab sync in plugin {}", env.name()); let action = Action::ToggleActiveSyncTab; apply_action!(action, error_msg, env); } fn close_focused_tab(env: &PluginEnv) { let error_msg = || format!("failed to close active tab in plugin {}", env.name()); let action = Action::CloseTab; apply_action!(action, error_msg, env); } fn undo_rename_tab(env: &PluginEnv) { let error_msg = || format!("failed to undo rename tab in plugin {}", env.name()); let action = Action::UndoRenameTab; apply_action!(action, error_msg, env); } fn quit_zellij(env: &PluginEnv) { let error_msg = || format!("failed to quit zellij in plugin {}", env.name()); let action = Action::Quit; apply_action!(action, error_msg, env); } fn previous_swap_layout(env: &PluginEnv) { let error_msg = || format!("failed to switch swap layout in plugin {}", env.name()); let action = Action::PreviousSwapLayout; apply_action!(action, error_msg, env); } fn next_swap_layout(env: &PluginEnv) { let error_msg = || format!("failed to switch swap layout in plugin {}", env.name()); let action = Action::NextSwapLayout; apply_action!(action, error_msg, env); } fn go_to_tab_name(env: &PluginEnv, tab_name: String) { let error_msg = || format!("failed to change tab in plugin {}", env.name()); let create = false; let action = Action::GoToTabName { name: tab_name, create, }; apply_action!(action, error_msg, env); } fn focus_or_create_tab(env: &PluginEnv, tab_name: String) { let error_msg = || format!("failed to change or create tab in plugin {}", env.name()); let create = true; let action = Action::GoToTabName { name: tab_name, create, }; let result = apply_action!(action, error_msg, env); // Return Some(tab_id) if tab was created or focused, None on error let tab_id: FocusOrCreateTabResponse = result.and_then(|r| r.affected_tab_id); let response = ProtobufFocusOrCreateTabResponse::from(tab_id); wasi_write_object(env, &response.encode_to_vec()).non_fatal(); } fn go_to_tab(env: &PluginEnv, tab_index: u32) { let error_msg = || format!("failed to change tab focus in plugin {}", env.name()); let action = Action::GoToTab { index: tab_index + 1, }; apply_action!(action, error_msg, env); } fn start_or_reload_plugin(env: &PluginEnv, url: &str) -> Result<()> { let error_msg = || format!("failed to start or reload plugin in plugin {}", env.name()); let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let run_plugin_or_alias = RunPluginOrAlias::from_url(url, &None, None, Some(cwd)) .map_err(|e| anyhow!("Failed to parse plugin location: {}", e))?; let action = Action::StartOrReloadPlugin { plugin: run_plugin_or_alias, }; apply_action!(action, error_msg, env); Ok(()) } fn close_terminal_pane(env: &PluginEnv, terminal_pane_id: u32) { let error_msg = || format!("failed to change tab focus in plugin {}", env.name()); let action = Action::CloseTerminalPane { pane_id: terminal_pane_id, }; apply_action!(action, error_msg, env); env.senders .send_to_pty(PtyInstruction::ClosePane( PaneId::Terminal(terminal_pane_id), None, )) .non_fatal(); } fn close_plugin_pane(env: &PluginEnv, plugin_pane_id: u32) { let error_msg = || format!("failed to change tab focus in plugin {}", env.name()); let action = Action::ClosePluginPane { pane_id: plugin_pane_id, }; apply_action!(action, error_msg, env); env.senders .send_to_plugin(PluginInstruction::Unload(plugin_pane_id)) .non_fatal(); } fn focus_terminal_pane( env: &PluginEnv, terminal_pane_id: u32, should_float_if_hidden: bool, should_be_in_place_if_hidden: bool, ) { let action = Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus terminal pane"); apply_action!(action, error_msg, env); } fn focus_plugin_pane( env: &PluginEnv, plugin_pane_id: u32, should_float_if_hidden: bool, should_be_in_place_if_hidden: bool, ) { let action = Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus plugin pane"); apply_action!(action, error_msg, env); } fn rename_terminal_pane(env: &PluginEnv, terminal_pane_id: u32, new_name: &str) { let error_msg = || format!("Failed to rename terminal pane"); let rename_pane_action = Action::RenameTerminalPane { pane_id: terminal_pane_id, name: new_name.as_bytes().to_vec(), }; apply_action!(rename_pane_action, error_msg, env); } fn rename_plugin_pane(env: &PluginEnv, plugin_pane_id: u32, new_name: &str) { let error_msg = || format!("Failed to rename plugin pane"); let rename_pane_action = Action::RenamePluginPane { pane_id: plugin_pane_id, name: new_name.as_bytes().to_vec(), }; apply_action!(rename_pane_action, error_msg, env); } fn rename_tab(env: &PluginEnv, tab_index: u32, new_name: &str) { let error_msg = || format!("Failed to rename tab"); let rename_tab_action = Action::RenameTab { tab_index, name: new_name.as_bytes().to_vec(), }; apply_action!(rename_tab_action, error_msg, env); } fn rename_session(env: &PluginEnv, new_session_name: String) { let error_msg = || format!("failed to rename session in plugin {}", env.name()); if new_session_name.contains('/') { log::error!("Session names cannot contain \'/\'"); } else { let action = Action::RenameSession { name: new_session_name, }; apply_action!(action, error_msg, env); } } fn disconnect_other_clients(env: &PluginEnv) { let _ = env .senders .send_to_server(ServerInstruction::DisconnectAllClientsExcept(env.client_id)) .context("failed to send disconnect other clients instruction"); } fn kill_sessions(session_names: Vec) { for session_name in session_names { let path = &*ZELLIJ_SOCK_DIR.join(&session_name); match ipc_connect(path) { Ok(stream) => { #[cfg(windows)] { use zellij_utils::consts::ipc_connect_reply; use zellij_utils::ipc::{IpcReceiverWithContext, ServerToClientMsg}; let reply = ipc_connect_reply(path); let _ = IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession); if let Ok(reply_stream) = reply { let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(reply_stream); let _ = receiver.recv_server_msg(); } } #[cfg(not(windows))] { let _ = IpcSenderWithContext::::new(stream) .send_client_msg(ClientToServerMsg::KillSession); } }, Err(e) => { log::error!("Failed to kill session {}: {:?}", session_name, e); }, }; } } // Wedge timeout: only guards against a peer that neither shuts down nor // crashes. Normal kill latency is tens of milliseconds (peer's route loop // reads the message, server sends Exit back), so 500 ms is several times the // expected worst case while still feeling instant to the user. Applied as a // single budget over the whole batch -- per-session kills are issued // concurrently so killing many sessions does not multiply the wait. const KILL_WEDGE_TIMEOUT: Duration = Duration::from_millis(500); fn kill_sessions_and_reply(env: &PluginEnv, session_names: Vec) { use tokio::task::JoinSet; let runtime = get_tokio_runtime(); let result: Result<(), String> = runtime.block_on(async { let mut set: JoinSet<(String, std::io::Result<()>)> = JoinSet::new(); for name in session_names { let path = ZELLIJ_SOCK_DIR.join(&name); set.spawn(async move { let res = zellij_utils::ipc::async_send_kill_and_await(&path).await; (name, res) }); } let drain = async { let mut first_err: Option = None; while let Some(joined) = set.join_next().await { match joined { Ok((_name, Ok(()))) => {}, Ok((name, Err(e))) => { if first_err.is_none() { first_err = Some(format!("Failed to kill session {}: {}", name, e)); } }, Err(e) => { if first_err.is_none() { first_err = Some(format!("Internal error in kill task: {}", e)); } }, } } match first_err { Some(e) => Err(e), None => Ok(()), } }; match tokio::time::timeout(KILL_WEDGE_TIMEOUT, drain).await { Ok(res) => res, Err(_) => { Err("Timed out waiting for one or more sessions to acknowledge kill".to_string()) }, } }); let response = match result { Ok(()) => KillSessionsResponse::Ok, Err(e) => KillSessionsResponse::Err(e), }; let protobuf_response = ProtobufKillSessionsResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| "failed to write kill_sessions response".to_string()) .non_fatal(); } fn delete_dead_session_and_reply(env: &PluginEnv, session_name: String) { let response = match std::fs::remove_dir_all(&*ZELLIJ_SESSION_INFO_CACHE_DIR.join(&session_name)) { Ok(()) => DeleteDeadSessionResponse::Ok, Err(e) => DeleteDeadSessionResponse::Err(format!( "Failed to delete dead session {}: {}", session_name, e )), }; let protobuf_response = ProtobufDeleteDeadSessionResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| "failed to write delete_dead_session response".to_string()) .non_fatal(); } fn delete_all_dead_sessions_and_reply(env: &PluginEnv) { // Same budget as kill-all so the UX of "y to confirm" is consistent: the // host-side fs work for many dead sessions is bounded in wall time. let runtime = get_tokio_runtime(); let result: Result<(), String> = runtime.block_on(async { let fs_task = tokio::task::spawn_blocking(delete_all_dead_sessions); match tokio::time::timeout(KILL_WEDGE_TIMEOUT, fs_task).await { Ok(Ok(Ok(()))) => Ok(()), Ok(Ok(Err(e))) => Err(format!("Failed to delete dead sessions: {}", e)), Ok(Err(e)) => Err(format!("Internal error in delete-all task: {}", e)), Err(_) => Err("Timed out deleting dead sessions".to_string()), } }); let response = match result { Ok(()) => DeleteAllDeadSessionsResponse::Ok, Err(e) => DeleteAllDeadSessionsResponse::Err(e), }; let protobuf_response = ProtobufDeleteAllDeadSessionsResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| "failed to write delete_all_dead_sessions response".to_string()) .non_fatal(); } fn watch_filesystem(env: &PluginEnv) { let _ = env .senders .to_plugin .as_ref() .map(|sender| sender.send(PluginInstruction::WatchFilesystem)); } // note this removes the requesting plugin fn dump_session_layout(env: &PluginEnv, tab_index: Option) { use crate::plugins::DumpSessionLayoutResponse; // Create oneshot channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Send request to screen thread let send_result = env .senders .send_to_screen(ScreenInstruction::DumpLayoutToPlugin { plugin_id: env.plugin_id, tab_index, response_channel: response_sender, }); if let Err(e) = send_result { let error_response = ProtobufDumpSessionLayoutResponse { result: Some(dump_session_layout_response::Result::Error(format!( "Failed to send dump layout request: {}", e ))), metadata: None, }; let _ = wasi_write_object(env, &error_response.encode_to_vec()); return; } // Wait for response with 1 second timeout let response: DumpSessionLayoutResponse = match response_receiver.recv_timeout(Duration::from_secs(1)) { Ok(resp) => resp, Err(e) => { let error_response = ProtobufDumpSessionLayoutResponse { result: Some(dump_session_layout_response::Result::Error(format!( "Timeout waiting for session layout: {}", e ))), metadata: None, }; let _ = wasi_write_object(env, &error_response.encode_to_vec()); return; }, }; // Convert LayoutMetadata to protobuf let protobuf_metadata = response .metadata .and_then(|metadata| match metadata.try_into() { Ok(pb) => Some(pb), Err(e) => { log::error!("Failed to convert LayoutMetadata to protobuf: {}", e); None }, }); // Build protobuf response let protobuf_response = match response.layout_result { Ok(layout_content) => ProtobufDumpSessionLayoutResponse { result: Some(dump_session_layout_response::Result::LayoutContent( layout_content, )), metadata: protobuf_metadata, }, Err(error) => ProtobufDumpSessionLayoutResponse { result: Some(dump_session_layout_response::Result::Error(error)), metadata: None, }, }; // Write response back to plugin via WASI pipe let _ = wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| format!("failed to send session layout to plugin {}", env.name())); } fn save_session(env: &PluginEnv) { use save_session_response::Result as SaveSessionResult; let (completion_tx, completion_rx) = oneshot::channel(); let send_result = env.senders.send_to_screen(ScreenInstruction::SaveSession( env.client_id, Some(NotificationEnd::new(completion_tx)), )); let response = if let Err(e) = send_result { ProtobufSaveSessionResponse { result: Some(SaveSessionResult::Error(format!( "Failed to send save request: {}", e ))), } } else { let wait_forever = false; let _result = wait_for_action_completion(completion_rx, "save_session", wait_forever); ProtobufSaveSessionResponse { result: Some(SaveSessionResult::Success(true)), } }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn show_floating_panes(env: &PluginEnv, tab_id: Option) { use show_floating_panes_response::Result as ShowResult; let (completion_tx, completion_rx) = oneshot::channel(); let send_result = env .senders .send_to_screen(ScreenInstruction::ShowFloatingPanes { client_id: env.client_id, tab_id, completion: Some(NotificationEnd::new(completion_tx)), }); let response = if let Err(e) = send_result { ProtobufShowFloatingPanesResponse { result: Some(ShowResult::Error(format!("{}", e))), } } else { let result = wait_for_action_completion(completion_rx, "show_floating_panes", false); match result.exit_status { Some(0) => ProtobufShowFloatingPanesResponse { result: Some(ShowResult::Success(true)), }, Some(2) => ProtobufShowFloatingPanesResponse { result: Some(ShowResult::Success(false)), }, _ => ProtobufShowFloatingPanesResponse { result: Some(ShowResult::Error("Tab not found".to_string())), }, } }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn hide_floating_panes(env: &PluginEnv, tab_id: Option) { use hide_floating_panes_response::Result as HideResult; let (completion_tx, completion_rx) = oneshot::channel(); let send_result = env .senders .send_to_screen(ScreenInstruction::HideFloatingPanes { client_id: env.client_id, tab_id, completion: Some(NotificationEnd::new(completion_tx)), }); let response = if let Err(e) = send_result { ProtobufHideFloatingPanesResponse { result: Some(HideResult::Error(format!("{}", e))), } } else { let result = wait_for_action_completion(completion_rx, "hide_floating_panes", false); match result.exit_status { Some(0) => ProtobufHideFloatingPanesResponse { result: Some(HideResult::Success(true)), }, Some(2) => ProtobufHideFloatingPanesResponse { result: Some(HideResult::Success(false)), }, _ => ProtobufHideFloatingPanesResponse { result: Some(HideResult::Error("Tab not found".to_string())), }, } }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn set_pane_regex_highlights( env: &PluginEnv, pane_id: zellij_utils::data::PaneId, highlights: Vec, ) { let err_context = || "failed to set pane regex highlights".to_string(); let pane_id: PaneId = pane_id.into(); env.senders .send_to_screen(ScreenInstruction::SetPluginRegexHighlights { pane_id, plugin_id: env.plugin_id, highlights, }) .with_context(err_context) .non_fatal(); } fn clear_pane_highlights(env: &PluginEnv, pane_id: zellij_utils::data::PaneId) { let err_context = || "failed to clear pane highlights".to_string(); let pane_id: PaneId = pane_id.into(); env.senders .send_to_screen(ScreenInstruction::ClearPluginHighlights { pane_id, plugin_id: env.plugin_id, }) .with_context(err_context) .non_fatal(); } fn current_session_last_saved_time(env: &PluginEnv) { use zellij_utils::plugin_api::plugin_command::ProtobufCurrentSessionLastSavedTimeResponse; let (response_tx, response_rx) = crossbeam::channel::bounded(1); let send_result = env .senders .send_to_plugin(PluginInstruction::GetLastSessionSaveTime { response_channel: response_tx, }); let saved_timestamp_millis = if send_result.is_ok() { response_rx .recv_timeout(std::time::Duration::from_secs(1)) .ok() .flatten() } else { None }; let timestamp_millis = saved_timestamp_millis.map(|saved_time| { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; now.saturating_sub(saved_time) }); let response = ProtobufCurrentSessionLastSavedTimeResponse { timestamp_millis }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn get_pane_info(env: &PluginEnv, pane_id: zellij_utils::data::PaneId) { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; let err_context = || { format!( "failed to get pane info for pane {:?} from plugin {}", pane_id, env.name() ) }; // Create channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Convert from plugin PaneId to server PaneId let server_pane_id: PaneId = pane_id.into(); // Send request to screen thread env.senders .send_to_screen(ScreenInstruction::GetPaneInfo { pane_id: server_pane_id, response_channel: response_sender, }) .with_context(err_context) .non_fatal(); // Block waiting for response with 100ms timeout let pane_info = match response_receiver.recv_timeout(Duration::from_millis(100)) { Ok(Some(pane_info)) => { // Convert PaneInfo to ProtobufPaneInfo match pane_info.try_into() { Ok(protobuf_pane_info) => Some(protobuf_pane_info), Err(e) => { log::error!( "Failed to convert PaneInfo to protobuf for pane {:?}: {}", pane_id, e ); None }, } }, Ok(None) => None, // Pane not found Err(RecvTimeoutError::Timeout) => { log::error!( "GetPaneInfo timed out for pane {:?} from plugin {}", pane_id, env.plugin_id ); None }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetPaneInfo channel disconnected for plugin {}", env.plugin_id ); None }, }; let response = ProtobufGetPaneInfoResponse { pane_info }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn get_tab_info(env: &PluginEnv, tab_id: usize) { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; let err_context = || { format!( "failed to get tab info for tab {} from plugin {}", tab_id, env.name() ) }; // Create channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Send request to screen thread env.senders .send_to_screen(ScreenInstruction::GetTabInfo { tab_id, response_channel: response_sender, }) .with_context(err_context) .non_fatal(); // Block waiting for response with 100ms timeout let tab_info = match response_receiver.recv_timeout(Duration::from_millis(100)) { Ok(Some(tab_info)) => { // Convert TabInfo to ProtobufTabInfo match tab_info.try_into() { Ok(protobuf_tab_info) => Some(protobuf_tab_info), Err(e) => { log::error!( "Failed to convert TabInfo to protobuf for tab {}: {}", tab_id, e ); None }, } }, Ok(None) => None, // Tab not found Err(RecvTimeoutError::Timeout) => { log::error!( "GetTabInfo timed out for tab {} from plugin {}", tab_id, env.plugin_id ); None }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetTabInfo channel disconnected for plugin {}", env.plugin_id ); None }, }; let response = ProtobufGetTabInfoResponse { tab_info }; let _ = wasi_write_object(env, &response.encode_to_vec()); } fn list_clients(env: &PluginEnv) { let _ = env.senders.to_screen.as_ref().map(|sender| { sender.send(ScreenInstruction::ListClientsToPlugin( env.plugin_id, env.client_id, )) }); } fn change_host_folder(env: &PluginEnv, new_host_folder: PathBuf) { let _ = env.senders.to_plugin.as_ref().map(|sender| { sender.send(PluginInstruction::ChangePluginHostDir( new_host_folder, env.plugin_id, env.client_id, )) }); } fn set_floating_pane_pinned(env: &PluginEnv, pane_id: PaneId, should_be_pinned: bool) { let _ = env.senders.to_screen.as_ref().map(|sender| { sender.send(ScreenInstruction::SetFloatingPanePinned( pane_id, should_be_pinned, )) }); } fn stack_panes(env: &PluginEnv, pane_ids: Vec) { let _ = env.senders .send_to_screen(ScreenInstruction::StackPanes(pane_ids, env.client_id, None)); } fn change_floating_panes_coordinates( env: &PluginEnv, pane_ids_and_coordinates: Vec<(PaneId, FloatingPaneCoordinates)>, ) { let _ = env .senders .send_to_screen(ScreenInstruction::ChangeFloatingPanesCoordinates( pane_ids_and_coordinates, None, )); } fn toggle_pane_borderless(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::TogglePaneBorderless(pane_id, None)); } fn set_pane_borderless(env: &PluginEnv, pane_id: PaneId, borderless: bool) { let _ = env .senders .send_to_screen(ScreenInstruction::SetPaneBorderless( pane_id, borderless, None, )); } fn set_pane_color(env: &PluginEnv, pane_id: PaneId, fg: Option, bg: Option) { let _ = env .senders .send_to_screen(ScreenInstruction::SetPaneColor(pane_id, fg, bg, None)); } fn scan_host_folder(env: &PluginEnv, folder_to_scan: PathBuf) { if !folder_to_scan.starts_with("/host") { log::error!( "Can only scan files in the /host filesystem, found: {}", folder_to_scan.display() ); return; } let plugin_host_folder = env .plugin_cwd .canonicalize() .unwrap_or_else(|_| env.plugin_cwd.clone()); let folder_to_scan = plugin_host_folder.join(folder_to_scan.strip_prefix("/host").unwrap()); match folder_to_scan.canonicalize() { Ok(folder_to_scan) => { if !folder_to_scan.starts_with(&plugin_host_folder) { log::error!( "Can only scan files in the plugin filesystem: {}, found: {}", plugin_host_folder.display(), folder_to_scan.display() ); return; } let reading_folder = std::fs::read_dir(&folder_to_scan); match reading_folder { Ok(reading_folder) => { let send_plugin_instructions = env.senders.to_plugin.clone(); let update_target = Some(env.plugin_id); let client_id = env.client_id; thread::spawn({ move || { let mut paths_in_folder = vec![]; for entry in reading_folder { if let Ok(entry) = entry { let entry_metadata = entry.metadata().ok().map(|m| m.into()); paths_in_folder.push(( PathBuf::from("/host").join( entry.path().strip_prefix(&plugin_host_folder).unwrap(), ), entry_metadata.into(), )); } } let _ = send_plugin_instructions .ok_or(anyhow!("found no sender to send plugin instruction to")) .map(|sender| { let _ = sender.send(PluginInstruction::Update(vec![( update_target, Some(client_id), Event::FileSystemUpdate(paths_in_folder), )])); }) .non_fatal(); } }); }, Err(e) => { log::error!("Failed to read folder {}: {e}", folder_to_scan.display()); }, } }, Err(e) => { log::error!( "Failed to canonicalize path {folder_to_scan:?} when scanning folder: {:?}", e ); }, } } #[cfg(not(windows))] fn list_windows_volumes(_env: &PluginEnv) { log::error!("ListWindowsVolumes is only supported on Windows"); } fn set_soft_keyboard(env: &PluginEnv, on: bool) { env.senders .send_to_screen(ScreenInstruction::SetSoftKeyboard { client_id: env.client_id, on, }) .with_context(|| { format!( "failed to dispatch SetSoftKeyboard for plugin {}", env.plugin_id ) }) .non_fatal(); } fn set_shadow_focus(env: &PluginEnv, pane_id: PaneId) { env.senders .send_to_screen(ScreenInstruction::SetShadowFocus(env.client_id, pane_id)) .with_context(|| { format!( "failed to dispatch SetShadowFocus for plugin {}", env.plugin_id ) }) .non_fatal(); } fn set_tab_fit(env: &PluginEnv, tab_id: usize, fit: Option<(PaneId, Size)>) { env.senders .send_to_screen(ScreenInstruction::SetTabFit { client_id: env.client_id, tab_id, fit, }) .with_context(|| format!("failed to dispatch SetTabFit for plugin {}", env.plugin_id)) .non_fatal(); } fn exit_mobile_mode(env: &PluginEnv) { env.senders .send_to_screen(ScreenInstruction::ExitMobileMode(env.client_id, None)) .with_context(|| { format!( "failed to dispatch ExitMobileMode for plugin {}", env.plugin_id ) }) .non_fatal(); } #[cfg(windows)] fn list_windows_volumes(env: &PluginEnv) { let send_plugin_instructions = env.senders.to_plugin.clone(); let update_target = Some(env.plugin_id); let client_id = env.client_id; thread::spawn(move || { let mut entries = enumerate_drives(); entries.extend(enumerate_wsl_distributions()); let _ = send_plugin_instructions .ok_or(anyhow!("found no sender to send plugin instruction to")) .map(|sender| { let _ = sender.send(PluginInstruction::Update(vec![( update_target, Some(client_id), Event::FileSystemUpdate(entries), )])); }) .non_fatal(); }); } #[cfg(windows)] fn enumerate_drives() -> Vec<(PathBuf, Option)> { use windows_sys::Win32::Storage::FileSystem::GetLogicalDriveStringsW; let mut buf = [0u16; 256]; let len = unsafe { GetLogicalDriveStringsW(buf.len() as u32, buf.as_mut_ptr()) }; if len == 0 { return vec![]; } let drive_strings = &buf[..len as usize]; let mut entries = vec![]; for chunk in drive_strings.split(|&c| c == 0) { if chunk.is_empty() { continue; } let drive = String::from_utf16_lossy(chunk); let normalized = drive.replace('\\', "/"); let drive_path = PathBuf::from(&normalized); let metadata = drive_path.metadata().ok().map(|m| m.into()); entries.push((drive_path, metadata)); } entries } #[cfg(windows)] fn enumerate_wsl_distributions() -> Vec<(PathBuf, Option)> { // read_dir on \\wsl.localhost\ or \\wsl$\ can fail with OS error 64/67 // even when WSL is running, so enumerate distros via `wsl -l -q` instead. let output = match std::process::Command::new("wsl") .args(["-l", "-q"]) .output() { Ok(o) if o.status.success() => o, _ => return vec![], }; // wsl -l -q outputs UTF-16LE on Windows let stdout: Vec = output .stdout .chunks(2) .map(|c| u16::from_le_bytes([c[0], *c.get(1).unwrap_or(&0)])) .collect(); let text = String::from_utf16_lossy(&stdout); let mut entries = vec![]; for line in text.lines() { let name = line.trim().trim_matches('\0'); if name.is_empty() { continue; } // Verify the distro is reachable before listing it let unc = format!("\\\\wsl.localhost\\{}", name); let metadata = std::fs::metadata(&unc).ok().map(|m| m.into()); let wsl_path = PathBuf::from(format!("//wsl.localhost/{}/", name)); entries.push((wsl_path, metadata)); } entries } fn resize_pane_with_id(env: &PluginEnv, resize: ResizeStrategy, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ResizePaneWithId(resize, pane_id)); } fn edit_scrollback_for_pane_with_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::EditScrollbackForPaneWithId( pane_id, None, )); } fn get_pane_scrollback(env: &PluginEnv, pane_id: PaneId, get_full_scrollback: bool) { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; let err_context = || { format!( "failed to get pane scrollback for pane {:?} from plugin {}", pane_id, env.name() ) }; // Create oneshot channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Send request to screen thread env.senders .send_to_screen(ScreenInstruction::GetPaneScrollback { pane_id, client_id: env.client_id, get_full_scrollback, response_channel: response_sender, }) .with_context(err_context) .non_fatal(); // Block waiting for response with 5 second timeout let response = match response_receiver.recv_timeout(Duration::from_secs(5)) { Ok(response) => response, Err(RecvTimeoutError::Timeout) => { log::error!( "GetPaneScrollback timed out after 5s for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); PaneScrollbackResponse::Err(format!( "Timeout retrieving scrollback for pane {:?}", pane_id )) }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetPaneScrollback channel disconnected for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); PaneScrollbackResponse::Err(format!( "Channel disconnected while retrieving scrollback for pane {:?}", pane_id )) }, }; // Convert to protobuf and write back to plugin ProtobufPaneScrollbackResponse::try_from(response) .map_err(|e| anyhow!("Failed to serialize pane scrollback response: {}", e)) .and_then(|serialized| { wasi_write_object(env, &serialized.encode_to_vec())?; Ok(()) }) .with_context(err_context) .non_fatal(); } fn write_to_pane_id(env: &PluginEnv, bytes: Vec, pane_id: PaneId) { let (completion_tx, completion_rx) = oneshot::channel(); let send_result = env.senders.send_to_screen(ScreenInstruction::WriteToPaneId( bytes, pane_id, Some(NotificationEnd::new(completion_tx)), )); if send_result.is_ok() { let wait_forever = false; let _ = wait_for_action_completion(completion_rx, "write_to_pane_id", wait_forever); } } fn write_chars_to_pane_id(env: &PluginEnv, chars: String, pane_id: PaneId) { let bytes = chars.into_bytes(); let (completion_tx, completion_rx) = oneshot::channel(); let send_result = env.senders.send_to_screen(ScreenInstruction::WriteToPaneId( bytes, pane_id, Some(NotificationEnd::new(completion_tx)), )); if send_result.is_ok() { let wait_forever = false; let _ = wait_for_action_completion(completion_rx, "write_chars_to_pane_id", wait_forever); } } fn send_sigint_to_pane_id(env: &PluginEnv, pane_id: PaneId) { let err_context = || { format!( "failed to send SIGINT to pane {:?} from plugin {}", pane_id, env.name() ) }; env.senders .send_to_pty(PtyInstruction::SendSigintToPaneId(pane_id)) .with_context(err_context) .non_fatal(); } fn send_sigkill_to_pane_id(env: &PluginEnv, pane_id: PaneId) { let err_context = || { format!( "failed to send SIGKILL to pane {:?} from plugin {}", pane_id, env.name() ) }; env.senders .send_to_pty(PtyInstruction::SendSigkillToPaneId(pane_id)) .with_context(err_context) .non_fatal(); } fn get_pane_pid(env: &PluginEnv, pane_id: PaneId) { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; let err_context = || { format!( "failed to get PID for pane {:?} from plugin {}", pane_id, env.name() ) }; // Create oneshot channel for response let (response_sender, response_receiver) = crossbeam::channel::bounded(1); // Send request directly to PTY thread env.senders .send_to_pty(PtyInstruction::GetPanePid { pane_id, response_channel: response_sender, }) .with_context(err_context) .non_fatal(); // Block waiting for response with 100ms timeout let response = match response_receiver.recv_timeout(Duration::from_millis(100)) { Ok(response) => response, Err(RecvTimeoutError::Timeout) => { log::error!( "GetPanePid timed out after 5s for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPanePidResponse::Err(format!("Timeout retrieving PID for pane {:?}", pane_id)) }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetPanePid channel disconnected for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPanePidResponse::Err(format!( "Channel disconnected while retrieving PID for pane {:?}", pane_id )) }, }; // Encode response and write to plugin's stdin let protobuf_response = ProtobufGetPanePidResponse::from(response); let serialized = protobuf_response.encode_to_vec(); wasi_write_object(env, &serialized) .with_context(err_context) .non_fatal(); } fn get_pane_running_command(env: &PluginEnv, pane_id: PaneId) { let (sender, receiver) = crossbeam::channel::bounded(1); send_get_pane_running_command_request(env, pane_id, sender); let response = await_pane_running_command_response(env, pane_id, receiver); write_pane_running_command_response(env, pane_id, response); } fn get_session_list(env: &PluginEnv) { use crate::background_jobs::{scan_session_list_default_dirs, session_scan_state}; use zellij_utils::data::{GetSessionListResponse, SessionListSnapshot}; let response = match session_scan_state() { Some(state) => { let (session_name, available_layouts, plugin_list) = { let name = state.current_session_name.lock().unwrap().clone(); let info = state.current_session_info.lock().unwrap().clone(); let plugins = state.current_session_plugin_list.lock().unwrap().clone(); (name, info.available_layouts, plugins) }; let (live_sessions_map, resurrectable_sessions_map) = scan_session_list_default_dirs(&session_name, &available_layouts, &plugin_list); let _ = env .senders .send_to_screen(ScreenInstruction::UpdateSessionInfos( live_sessions_map.clone(), resurrectable_sessions_map.clone(), )); let snapshot = SessionListSnapshot { live_sessions: live_sessions_map.into_values().collect(), resurrectable_sessions: resurrectable_sessions_map.into_iter().collect(), }; GetSessionListResponse::Ok(snapshot) }, None => GetSessionListResponse::Err("Session-scan state not initialized".to_string()), }; let protobuf_response = ProtobufGetSessionListResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| "failed to write get_session_list response".to_string()) .non_fatal(); } fn send_get_pane_running_command_request( env: &PluginEnv, pane_id: PaneId, sender: crossbeam::channel::Sender, ) { env.senders .send_to_pty(PtyInstruction::GetPaneRunningCommand { pane_id, response_channel: sender, }) .with_context(|| format!("Failed to get running command for pane {:?}", pane_id)) .non_fatal(); } fn await_pane_running_command_response( env: &PluginEnv, pane_id: PaneId, receiver: crossbeam::channel::Receiver, ) -> GetPaneRunningCommandResponse { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; match receiver.recv_timeout(Duration::from_millis(100)) { Ok(response) => response, Err(RecvTimeoutError::Timeout) => { log::error!( "GetPaneRunningCommand timed out for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPaneRunningCommandResponse::Err(format!( "Timeout retrieving running command for pane {:?}", pane_id )) }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetPaneRunningCommand channel disconnected for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPaneRunningCommandResponse::Err(format!( "Channel disconnected while retrieving running command for pane {:?}", pane_id )) }, } } fn write_pane_running_command_response( env: &PluginEnv, pane_id: PaneId, response: GetPaneRunningCommandResponse, ) { let protobuf_response = ProtobufGetPaneRunningCommandResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| format!("Failed to get running command for pane {:?}", pane_id)) .non_fatal(); } fn get_pane_cwd(env: &PluginEnv, pane_id: PaneId) { let (sender, receiver) = crossbeam::channel::bounded(1); send_get_pane_cwd_request(env, pane_id, sender); let response = await_pane_cwd_response(env, pane_id, receiver); write_pane_cwd_response(env, pane_id, response); } fn send_get_pane_cwd_request( env: &PluginEnv, pane_id: PaneId, sender: crossbeam::channel::Sender, ) { env.senders .send_to_pty(PtyInstruction::GetPaneCwd { pane_id, response_channel: sender, }) .with_context(|| format!("Failed to get CWD for pane {:?}", pane_id)) .non_fatal(); } fn await_pane_cwd_response( env: &PluginEnv, pane_id: PaneId, receiver: crossbeam::channel::Receiver, ) -> GetPaneCwdResponse { use crossbeam::channel::RecvTimeoutError; use std::time::Duration; match receiver.recv_timeout(Duration::from_millis(100)) { Ok(response) => response, Err(RecvTimeoutError::Timeout) => { log::error!( "GetPaneCwd timed out for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPaneCwdResponse::Err(format!("Timeout retrieving CWD for pane {:?}", pane_id)) }, Err(RecvTimeoutError::Disconnected) => { log::error!( "GetPaneCwd channel disconnected for plugin {} requesting pane {:?}", env.plugin_id, pane_id ); GetPaneCwdResponse::Err(format!( "Channel disconnected while retrieving CWD for pane {:?}", pane_id )) }, } } fn write_pane_cwd_response(env: &PluginEnv, pane_id: PaneId, response: GetPaneCwdResponse) { let protobuf_response = ProtobufGetPaneCwdResponse::from(response); wasi_write_object(env, &protobuf_response.encode_to_vec()) .with_context(|| format!("Failed to get CWD for pane {:?}", pane_id)) .non_fatal(); } fn save_layout(env: &PluginEnv, layout_name: String, layout_kdl: String, overwrite: bool) { let err_context = || format!("Failed to save layout '{}'", layout_name); let response = try_save_layout(env, &layout_name, &layout_kdl, overwrite) .map(|_| SaveLayoutResponse::Ok(())) .unwrap_or_else(SaveLayoutResponse::Err); // Serialize and send response back to plugin let protobuf_response = ProtobufSaveLayoutResponse::from(response); let serialized = protobuf_response.encode_to_vec(); wasi_write_object(env, &serialized) .with_context(err_context) .non_fatal(); } fn try_save_layout( env: &PluginEnv, layout_name: &str, layout_kdl: &str, overwrite: bool, ) -> Result<(), String> { // Step 1: Sanitize layout name for path traversal and invalid characters let safe_name = sanitize_layout_name(layout_name)?; // Step 2: Validate the layout by parsing it Layout::from_kdl( layout_kdl, Some(format!("{}.kdl", safe_name)), None, // No swap layouts None, // No cwd override ) .map_err(|config_error| format!("Invalid layout KDL: {:?}", config_error))?; // Step 3: Get layout_dir from PluginEnv let layout_dir = env .layout_dir .clone() .or_else(default_layout_dir) .ok_or_else(|| "Layout directory not found".to_string())?; // Step 4: Create file path let file_path = layout_dir.join(format!("{}.kdl", safe_name)); // Step 5: Check if file exists when overwrite=false if file_path.exists() && !overwrite { return Err(format!( "Layout file '{}' already exists. Use overwrite flag to replace it.", safe_name )); } // Step 6: Ensure layout directory exists std::fs::create_dir_all(layout_dir) .map_err(|e| format!("Failed to create layout directory: {}", e))?; // Step 7: Write to disk let mut parsed_layout: KdlDocument = layout_kdl.parse().unwrap(); // unwrap // should // be // safe, // but // let's // do // it // nicer parsed_layout.fmt(); std::fs::write(&file_path, &parsed_layout.to_string()) .map_err(|io_error| format!("Failed to write layout file: {}", io_error))?; Ok(()) } fn delete_layout(env: &PluginEnv, layout_name: String) { let err_context = || format!("Failed to delete layout '{}'", layout_name); let response = try_delete_layout(env, &layout_name) .map(|_| DeleteLayoutResponse::Ok(())) .unwrap_or_else(|e| DeleteLayoutResponse::Err(e)); let protobuf_response: ProtobufDeleteLayoutResponse = response.into(); let serialized = protobuf_response.encode_to_vec(); wasi_write_object(env, &serialized) .with_context(err_context) .non_fatal(); } fn rename_layout(env: &PluginEnv, old_layout_name: String, new_layout_name: String) { let err_context = || { format!( "Failed to rename layout '{}' to '{}'", old_layout_name, new_layout_name ) }; let response = try_rename_layout(env, &old_layout_name, &new_layout_name) .map(|_| RenameLayoutResponse::Ok(())) .unwrap_or_else(|e| RenameLayoutResponse::Err(e)); let protobuf_response: ProtobufRenameLayoutResponse = response.into(); let serialized = protobuf_response.encode_to_vec(); wasi_write_object(env, &serialized) .with_context(err_context) .non_fatal(); } fn try_delete_layout(env: &PluginEnv, layout_name: &str) -> Result<(), String> { // Sanitize the layout name to prevent directory traversal let safe_name = sanitize_layout_name(layout_name).map_err(|e| format!("Invalid layout name: {}", e))?; // Get the layout directory from PluginEnv let layout_dir = env .layout_dir .clone() .or_else(default_layout_dir) .ok_or_else(|| "Layout directory not found".to_string())?; // Construct the full file path let file_path = layout_dir.join(format!("{}.kdl", safe_name)); // Check if the file exists if !file_path.exists() { return Err(format!("Layout '{}' not found", safe_name)); } // Delete the file std::fs::remove_file(&file_path).map_err(|e| format!("Failed to delete layout file: {}", e))?; Ok(()) } fn try_rename_layout( env: &PluginEnv, old_layout_name: &str, new_layout_name: &str, ) -> Result<(), String> { // Step 1: Sanitize both layout names let safe_old_name = sanitize_layout_name(old_layout_name) .map_err(|e| format!("Invalid old layout name: {}", e))?; let safe_new_name = sanitize_layout_name(new_layout_name) .map_err(|e| format!("Invalid new layout name: {}", e))?; // Step 2: Get layout directory from PluginEnv let layout_dir = env .layout_dir .clone() .or_else(default_layout_dir) .ok_or_else(|| "Layout directory not found".to_string())?; // Step 3: Construct file paths let old_file_path = layout_dir.join(format!("{}.kdl", safe_old_name)); let new_file_path = layout_dir.join(format!("{}.kdl", safe_new_name)); // Step 4: Check if source file exists if !old_file_path.exists() { return Err(format!("Layout '{}' not found", safe_old_name)); } // Step 5: Check if target file already exists (fail if it does - no overwrite) if new_file_path.exists() { return Err(format!( "Layout '{}' already exists. Cannot rename '{}' to existing layout name.", safe_new_name, safe_old_name )); } // Step 6: Rename the file std::fs::rename(&old_file_path, &new_file_path) .map_err(|e| format!("Failed to rename layout file: {}", e))?; Ok(()) } fn edit_layout(env: &PluginEnv, layout_name: String, context: BTreeMap) { let err_context = || format!("Failed to edit layout '{}'", layout_name); let response = try_edit_layout(env, &layout_name, context) .map(|_| EditLayoutResponse::Ok(())) .unwrap_or_else(|e| EditLayoutResponse::Err(e)); let protobuf_response: ProtobufEditLayoutResponse = response.into(); let serialized = protobuf_response.encode_to_vec(); wasi_write_object(env, &serialized) .with_context(err_context) .non_fatal(); } fn try_edit_layout( env: &PluginEnv, layout_name: &str, context: BTreeMap, ) -> Result<(), String> { // Sanitize the layout name to prevent directory traversal let safe_name = sanitize_layout_name(layout_name).map_err(|e| format!("Invalid layout name: {}", e))?; // Get the layout directory from PluginEnv let layout_dir = env .layout_dir .clone() .or_else(default_layout_dir) .ok_or_else(|| "Layout directory not found".to_string())?; // Construct the full file path let file_path = layout_dir.join(format!("{}.kdl", safe_name)); // Create FileToOpen for the layout file let file_to_open = FileToOpen { path: file_path, line_number: None, cwd: Some(layout_dir.clone()), }; // Create an Action::EditFile let action = Action::EditFile { payload: OpenFilePayload::new( file_to_open.path, file_to_open.line_number, file_to_open.cwd, ) .with_originating_plugin(OriginatingPlugin::new( env.plugin_id, env.client_id, context, )), direction: None, floating: false, in_place: true, close_replaced_pane: false, start_suppressed: false, coordinates: None, near_current_pane: true, no_focus: false, tab_id: None, }; // Route the action - this is fallible route_action( action, env.client_id, None, Some(PaneId::Plugin(env.plugin_id)), env.senders.clone(), env.default_shell.clone(), None, env.default_mode.clone(), None, ) .map(|_| ()) .map_err(|e| format!("Failed to route edit action: {:?}", e)) } /// Sanitize layout name to prevent path traversal and invalid filenames /// Returns the sanitized name or an error message fn sanitize_layout_name(name: &str) -> Result { // Check for empty name if name.is_empty() { return Err("Layout name cannot be empty".to_string()); } // Check for path separators (prevent directory traversal) if name.contains('/') || name.contains('\\') { return Err("Layout name cannot contain path separators".to_string()); } // Check for parent directory references if name.contains("..") { return Err("Layout name cannot contain '..'".to_string()); } // Check length (most filesystems have 255 char limit, leave room for .kdl) if name.len() > 250 { return Err("Layout name too long (max 250 characters)".to_string()); } // Only allow alphanumeric, hyphens, underscores, and spaces let valid_chars = name .chars() .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' '); if !valid_chars { return Err( "Layout name can only contain letters, numbers, hyphens, underscores, and spaces" .to_string(), ); } Ok(name.to_string()) } fn move_pane_with_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::MovePaneWithPaneId(pane_id)); } fn move_pane_with_pane_id_in_direction(env: &PluginEnv, pane_id: PaneId, direction: Direction) { let _ = env .senders .send_to_screen(ScreenInstruction::MovePaneWithPaneIdInDirection( pane_id, direction, )); } fn clear_screen_for_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ClearScreenForPaneId(pane_id)); } fn scroll_up_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ScrollUpInPaneId(pane_id)); } fn scroll_down_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ScrollDownInPaneId(pane_id)); } fn scroll_to_top_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ScrollToTopInPaneId(pane_id)); } fn scroll_to_bottom_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::ScrollToBottomInPaneId(pane_id)); } fn page_scroll_up_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::PageScrollUpInPaneId(pane_id)); } fn page_scroll_down_in_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::PageScrollDownInPaneId(pane_id)); } fn toggle_pane_id_fullscreen(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::TogglePaneIdFullscreen(pane_id)); } fn toggle_pane_embed_or_eject_for_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders .send_to_screen(ScreenInstruction::TogglePaneEmbedOrEjectForPaneId(pane_id)); } fn close_tab_with_index(env: &PluginEnv, tab_index: usize) { let _ = env .senders .send_to_screen(ScreenInstruction::CloseTabWithIndex(tab_index)); } fn break_panes_to_new_tab( env: &PluginEnv, pane_ids: Vec, new_tab_name: Option, should_change_focus_to_new_tab: bool, ) { let default_shell = env.default_shell.clone().or_else(|| { Some(TerminalAction::RunCommand(RunCommand { command: env.path_to_default_shell.clone(), use_terminal_title: true, ..Default::default() })) }); // Create completion channel to receive tab ID let (tx, rx) = tokio::sync::oneshot::channel(); let completion_tx = Some(NotificationEnd::new(tx)); let result = env .senders .send_to_screen(ScreenInstruction::BreakPanesToNewTab { pane_ids, default_shell, new_tab_name, should_change_focus_to_new_tab, client_id: env.client_id, completion_tx, }); let tab_id: BreakPanesToNewTabResponse = if result.is_ok() { wait_for_action_completion(rx, "break_panes_to_new_tab", false).affected_tab_id } else { None }; let response = ProtobufBreakPanesToNewTabResponse::from(tab_id); wasi_write_object(env, &response.encode_to_vec()).non_fatal(); } fn break_panes_to_tab_with_index( env: &PluginEnv, pane_ids: Vec, should_change_focus_to_new_tab: bool, tab_index: usize, ) { // Create completion channel to receive tab ID let (tx, rx) = tokio::sync::oneshot::channel(); let completion_tx = Some(NotificationEnd::new(tx)); let result = env .senders .send_to_screen(ScreenInstruction::BreakPanesToTabWithIndex { pane_ids, tab_index, client_id: env.client_id, should_change_focus_to_new_tab, completion_tx, }); let tab_id: BreakPanesToTabWithIndexResponse = if result.is_ok() { wait_for_action_completion(rx, "break_panes_to_tab_with_index", false).affected_tab_id } else { None }; let response = ProtobufBreakPanesToTabWithIndexResponse::from(tab_id); wasi_write_object(env, &response.encode_to_vec()).non_fatal(); } fn switch_tab_to_id(env: &PluginEnv, tab_id: u64) { let tab_id = tab_id as usize; env.senders .send_to_screen(ScreenInstruction::GoToTabWithId( tab_id, Some(env.client_id), None, )) .with_context(|| { format!( "failed to switch to tab {} from plugin {}", tab_id, env.name() ) }) .non_fatal(); } fn go_to_tab_with_id(env: &PluginEnv, tab_id: u64) { let tab_id = tab_id as usize; env.senders .send_to_screen(ScreenInstruction::GoToTabWithId( tab_id, Some(env.client_id), None, )) .with_context(|| format!("failed to go to tab {} from plugin {}", tab_id, env.name())) .non_fatal(); } fn close_tab_with_id(env: &PluginEnv, tab_id: u64) { let tab_id = tab_id as usize; let _ = env .senders .send_to_screen(ScreenInstruction::CloseTabWithId(tab_id, None)); } fn rename_tab_with_id(env: &PluginEnv, tab_id: u64, new_name: &str) { let tab_id = tab_id as usize; let rename_tab_action = ScreenInstruction::RenameTabWithId(tab_id, new_name.as_bytes().to_vec(), None); env.senders .send_to_screen(rename_tab_action) .with_context(|| format!("Failed to rename tab {}", tab_id)) .non_fatal(); } fn break_panes_to_tab_with_id( env: &PluginEnv, pane_ids: Vec, should_change_focus_to_target_tab: bool, tab_id: u64, ) { let tab_id = tab_id as usize; // Create completion channel to receive tab ID let (tx, rx) = tokio::sync::oneshot::channel(); let completion_tx = Some(NotificationEnd::new(tx)); let result = env .senders .send_to_screen(ScreenInstruction::BreakPanesToTabWithId { pane_ids, tab_id, should_change_focus_to_target_tab, client_id: env.client_id, completion_tx, }); let result_tab_id: BreakPanesToTabWithIdResponse = if result.is_ok() { wait_for_action_completion(rx, "break_panes_to_tab_with_id", false).affected_tab_id } else { None }; let response = ProtobufBreakPanesToTabWithIdResponse::from(result_tab_id); wasi_write_object(env, &response.encode_to_vec()).non_fatal(); } fn reload_plugin(env: &PluginEnv, plugin_id: u32) { let _ = env .senders .send_to_plugin(PluginInstruction::ReloadPluginWithId(plugin_id)); } fn load_new_plugin( env: &PluginEnv, url: String, config: BTreeMap, load_in_background: bool, skip_plugin_cache: bool, ) { let url = if &url == "zellij:OWN_URL" { env.plugin.location.display() } else { url }; if load_in_background { match RunPluginOrAlias::from_url(&url, &Some(config), None, Some(env.plugin_cwd.clone())) { Ok(run_plugin_or_alias) => { let _ = env .senders .send_to_plugin(PluginInstruction::LoadBackgroundPlugin( run_plugin_or_alias, env.client_id, )); }, Err(e) => { log::error!("Failed to load new plugin: {:?}", e); }, } } else { let should_float = Some(true); let should_be_open_in_place = false; let pane_title = None; let tab_index = None; let pane_id_to_replace = None; let client_id = env.client_id; let size = Default::default(); let cwd = Some(env.plugin_cwd.clone()); let skip_cache = skip_plugin_cache; match RunPluginOrAlias::from_url(&url, &Some(config), None, Some(env.plugin_cwd.clone())) { Ok(run_plugin_or_alias) => { let _ = env.senders.send_to_plugin(PluginInstruction::Load( should_float, should_be_open_in_place, false, // close_replaced_pane pane_title, run_plugin_or_alias, tab_index, pane_id_to_replace, client_id, size, cwd, None, skip_cache, None, None, None, )); }, Err(e) => { log::error!("Failed to load new plugin: {:?}", e); }, } } } fn start_web_server(env: &PluginEnv) { let _ = env .senders .send_to_server(ServerInstruction::StartWebServer(env.client_id)); } fn stop_web_server(_env: &PluginEnv) { #[cfg(feature = "web_server_capability")] let _ = shutdown_all_webserver_instances(); #[cfg(not(feature = "web_server_capability"))] log::error!("This instance of Zellij was compiled without web server capabilities"); } fn query_web_server_status(env: &PluginEnv) { let _ = env .senders .send_to_background_jobs(BackgroundJob::QueryZellijWebServerStatus); } fn share_current_session(env: &PluginEnv) { let _ = env .senders .send_to_server(ServerInstruction::ShareCurrentSession(env.client_id)); } fn stop_sharing_current_session(env: &PluginEnv) { let _ = env .senders .send_to_server(ServerInstruction::StopSharingCurrentSession(env.client_id)); } fn group_and_ungroup_panes( env: &PluginEnv, panes_to_group: Vec, panes_to_ungroup: Vec, for_all_clients: bool, ) { let _ = env .senders .send_to_screen(ScreenInstruction::GroupAndUngroupPanes( panes_to_group, panes_to_ungroup, for_all_clients, env.client_id, )); } fn highlight_and_unhighlight_panes( env: &PluginEnv, panes_to_highlight: Vec, panes_to_unhighlight: Vec, ) { let _ = env .senders .send_to_screen(ScreenInstruction::HighlightAndUnhighlightPanes( panes_to_highlight, panes_to_unhighlight, env.client_id, )); } fn close_multiple_panes(env: &PluginEnv, pane_ids: Vec) { for pane_id in pane_ids { match pane_id { PaneId::Terminal(terminal_pane_id) => { close_terminal_pane(env, terminal_pane_id); }, PaneId::Plugin(plugin_pane_id) => { close_plugin_pane(env, plugin_pane_id); }, } } } fn float_multiple_panes(env: &PluginEnv, pane_ids: Vec) { let _ = env .senders .send_to_screen(ScreenInstruction::FloatMultiplePanes( pane_ids, env.client_id, )); } fn embed_multiple_panes(env: &PluginEnv, pane_ids: Vec) { let _ = env .senders .send_to_screen(ScreenInstruction::EmbedMultiplePanes( pane_ids, env.client_id, )); } #[cfg(feature = "web_server_capability")] fn generate_web_login_token(env: &PluginEnv, token_label: Option, read_only: bool) { let serialized = match create_token(token_label, read_only) { Ok((token, token_label)) => CreateTokenResponse { token: Some(token), token_label: Some(token_label), error: None, }, Err(e) => CreateTokenResponse { token: None, token_label: None, error: Some(e.to_string()), }, }; let _ = wasi_write_object(env, &serialized.encode_to_vec()); } #[cfg(not(feature = "web_server_capability"))] fn generate_web_login_token(env: &PluginEnv, _token_label: Option, _read_only: bool) { log::error!("This version of Zellij was compiled without the web server capabilities!"); let empty_vec: Vec<&str> = vec![]; let _ = wasi_write_object(env, &empty_vec); } #[cfg(feature = "web_server_capability")] fn revoke_web_login_token(env: &PluginEnv, token_label: String) { let serialized = match revoke_token(&token_label) { Ok(true) => RevokeTokenResponse { successfully_revoked: true, error: None, }, Ok(false) => RevokeTokenResponse { successfully_revoked: false, error: Some(format!("Token with label {} not found", token_label)), }, Err(e) => RevokeTokenResponse { successfully_revoked: false, error: Some(e.to_string()), }, }; let _ = wasi_write_object(env, &serialized.encode_to_vec()); } #[cfg(not(feature = "web_server_capability"))] fn revoke_web_login_token(env: &PluginEnv, _token_label: String) { log::error!("This version of Zellij was compiled without the web server capabilities!"); let empty_vec: Vec<&str> = vec![]; let _ = wasi_write_object(env, &empty_vec); } #[cfg(feature = "web_server_capability")] fn revoke_all_web_login_tokens(env: &PluginEnv) { let serialized = match revoke_all_tokens() { Ok(_) => RevokeAllWebTokensResponse { successfully_revoked: true, error: None, }, Err(e) => RevokeAllWebTokensResponse { successfully_revoked: false, error: Some(e.to_string()), }, }; let _ = wasi_write_object(env, &serialized.encode_to_vec()); } #[cfg(not(feature = "web_server_capability"))] fn revoke_all_web_login_tokens(env: &PluginEnv) { log::error!("This version of Zellij was compiled without the web server capabilities!"); let empty_vec: Vec<&str> = vec![]; let _ = wasi_write_object(env, &empty_vec); } #[cfg(feature = "web_server_capability")] fn rename_web_login_token(env: &PluginEnv, old_name: String, new_name: String) { let serialized = match rename_token(&old_name, &new_name) { Ok(_) => RenameWebTokenResponse { successfully_renamed: true, error: None, }, Err(e) => RenameWebTokenResponse { successfully_renamed: false, error: Some(e.to_string()), }, }; let _ = wasi_write_object(env, &serialized.encode_to_vec()); } #[cfg(not(feature = "web_server_capability"))] fn rename_web_login_token(env: &PluginEnv, _old_name: String, _new_name: String) { log::error!("This version of Zellij was compiled without the web server capabilities!"); let empty_vec: Vec<&str> = vec![]; let _ = wasi_write_object(env, &empty_vec); } #[cfg(feature = "web_server_capability")] fn list_web_login_tokens(env: &PluginEnv) { let serialized = match list_tokens() { Ok(token_list) => ListTokensResponse { tokens: token_list.iter().map(|t| t.name.clone()).collect(), creation_times: token_list.iter().map(|t| t.created_at.clone()).collect(), read_only_flags: token_list.iter().map(|t| t.read_only).collect(), error: None, }, Err(e) => ListTokensResponse { tokens: vec![], creation_times: vec![], read_only_flags: vec![], error: Some(e.to_string()), }, }; let _ = wasi_write_object(env, &serialized.encode_to_vec()); } #[cfg(not(feature = "web_server_capability"))] fn list_web_login_tokens(env: &PluginEnv) { log::error!("This version of Zellij was compiled without the web server capabilities!"); let empty_vec: Vec<&str> = vec![]; let _ = wasi_write_object(env, &empty_vec); } fn set_self_mouse_selection_support(env: &PluginEnv, selection_support: bool) { env.senders .send_to_screen(ScreenInstruction::SetMouseSelectionSupport( PaneId::Plugin(env.plugin_id), selection_support, )) .with_context(|| { format!( "failed to set plugin {} selectable from plugin {}", selection_support, env.name() ) }) .non_fatal(); } fn intercept_key_presses(env: &mut PluginEnv) { env.intercepting_key_presses = true; let _ = env .senders .send_to_screen(ScreenInstruction::InterceptKeyPresses( env.plugin_id, env.client_id, )); } fn clear_key_presses_intercepts(env: &mut PluginEnv) { env.intercepting_key_presses = false; let _ = env .senders .send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(env.client_id)); } fn replace_pane_with_existing_pane( env: &mut PluginEnv, pane_to_replace: PaneId, existing_pane: PaneId, suppress_replaced_pane: bool, ) { let (completion_tx, completion_rx) = oneshot::channel(); let _ = env .senders .send_to_screen(ScreenInstruction::ReplacePaneWithExistingPane( pane_to_replace, existing_pane, suppress_replaced_pane, Some(NotificationEnd::new(completion_tx)), )); let _ = wait_for_action_completion(completion_rx, "replace_pane_with_existing_pane", false); } fn override_layout( env: &mut PluginEnv, layout_info: LayoutInfo, retain_existing_terminal_panes: bool, retain_existing_plugin_panes: bool, apply_only_to_active_tab: bool, context: BTreeMap, ) -> Result<()> { let layout = Layout::from_layout_info(&env.layout_dir, layout_info) .map_err(|e| anyhow!("Failed to parse layout: {:?}", e))?; // Convert all tabs to Vec let tabs: Vec = layout .tabs .iter() .enumerate() .map(|(index, (tab_name, tiled, floating))| TabLayoutInfo { tab_index: index, tab_name: tab_name.clone(), tiled_layout: tiled.clone(), floating_layouts: floating.clone(), swap_tiled_layouts: Some(layout.swap_tiled_layouts.clone()), swap_floating_layouts: Some(layout.swap_floating_layouts.clone()), }) .collect(); // If no tabs, create default tab let tabs = if tabs.is_empty() { let (tiled, floating) = layout.new_tab(); vec![TabLayoutInfo { tab_index: 0, tab_name: None, tiled_layout: tiled, floating_layouts: floating, swap_tiled_layouts: Some(layout.swap_tiled_layouts), swap_floating_layouts: Some(layout.swap_floating_layouts), }] } else { tabs }; let action = Action::OverrideLayout { tabs, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_active_tab, }; run_action(env, action, context); Ok(()) } // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the // code trying to deserialize an `Event` upon a plugin state update, we read some panic message, // formatted as string from the plugin. fn report_panic(env: &PluginEnv, msg: &str) { log::error!("PANIC IN PLUGIN!\n\r{}", msg); handle_plugin_crash(env.plugin_id, msg.to_owned(), env.senders.clone()); } // Helper Functions --------------------------------------------------------------------------------------------------- pub fn wasi_read_string(plugin_env: &PluginEnv) -> Result { let err_context = || format!("failed to read string from WASI env"); let mut buf = vec![]; plugin_env .stdout_pipe .lock() .unwrap() .read_to_end(&mut buf) .map_err(anyError::new) .with_context(err_context)?; let buf = String::from_utf8_lossy(&buf); // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c Ok(buf.replace("\n", "\n\r")) } pub fn wasi_write_string(plugin_env: &PluginEnv, buf: &str) -> Result<()> { let mut stdin = plugin_env.stdin_pipe.lock().unwrap(); writeln!(stdin, "{}\r", buf) .map_err(anyError::new) .with_context(|| format!("failed to write string to WASI env")) } pub fn wasi_write_object(plugin_env: &PluginEnv, object: &(impl Serialize + ?Sized)) -> Result<()> { serde_json::to_string(&object) .map_err(anyError::new) .and_then(|string| wasi_write_string(plugin_env, &string)) .with_context(|| format!("failed to serialize object for WASI env")) } pub fn wasi_read_bytes(plugin_env: &PluginEnv) -> Result> { wasi_read_string(plugin_env) .and_then(|string| serde_json::from_str(&string).map_err(anyError::new)) .with_context(|| format!("failed to deserialize object from WASI env")) } // TODO: move to permissions? fn check_command_permission( plugin_env: &PluginEnv, command: &PluginCommand, ) -> (PermissionStatus, Option) { if plugin_env.plugin.is_builtin() { // built-in plugins can do all the things because they're part of the application and // there's no use to deny them anything return (PermissionStatus::Granted, None); } let permission = match command { PluginCommand::OpenFile(..) | PluginCommand::OpenFileFloating(..) | PluginCommand::OpenFileNearPlugin(..) | PluginCommand::OpenFileFloatingNearPlugin(..) | PluginCommand::OpenFileInPlaceOfPlugin(..) | PluginCommand::OpenFileInPlace(..) | PluginCommand::OpenEditPaneInPlaceOfPaneId(..) => PermissionType::OpenFiles, PluginCommand::OpenTerminal(..) | PluginCommand::OpenTerminalNearPlugin(..) | PluginCommand::StartOrReloadPlugin(..) | PluginCommand::OpenTerminalFloating(..) | PluginCommand::OpenTerminalFloatingNearPlugin(..) | PluginCommand::OpenTerminalInPlace(..) | PluginCommand::OpenTerminalInPlaceOfPlugin(..) | PluginCommand::OpenPluginPaneInNewTab { .. } | PluginCommand::OpenPluginPaneFloating { .. } | PluginCommand::NewTiledPaneInTab { .. } | PluginCommand::OpenTerminalPaneInPlaceOfPaneId(..) => { PermissionType::OpenTerminalsOrPlugins }, PluginCommand::OpenCommandPane(..) | PluginCommand::OpenCommandPaneNearPlugin(..) | PluginCommand::OpenCommandPaneFloating(..) | PluginCommand::OpenCommandPaneFloatingNearPlugin(..) | PluginCommand::OpenCommandPaneInPlace(..) | PluginCommand::OpenCommandPaneInPlaceOfPlugin(..) | PluginCommand::OpenCommandPaneBackground(..) | PluginCommand::OpenCommandPaneInPlaceOfPaneId(..) | PluginCommand::RunCommand(..) | PluginCommand::ExecCmd(..) => PermissionType::RunCommands, PluginCommand::WebRequest(..) => PermissionType::WebAccess, PluginCommand::Write(..) | PluginCommand::WriteChars(..) | PluginCommand::WriteToPaneId(..) | PluginCommand::WriteCharsToPaneId(..) => PermissionType::WriteToStdin, PluginCommand::CopyToClipboard(..) => PermissionType::WriteToClipboard, PluginCommand::SwitchTabTo(..) | PluginCommand::SwitchToMode(..) | PluginCommand::NewTabsWithLayout(..) | PluginCommand::NewTabsWithLayoutInfo(..) | PluginCommand::NewTab { .. } | PluginCommand::NewTabUnfocused { .. } | PluginCommand::GoToNextTab | PluginCommand::GoToPreviousTab | PluginCommand::Resize(..) | PluginCommand::ResizeWithDirection(..) | PluginCommand::FocusNextPane | PluginCommand::MoveFocus(..) | PluginCommand::MoveFocusOrTab(..) | PluginCommand::Detach | PluginCommand::EditScrollback | PluginCommand::EditScrollbackForPaneWithId(..) | PluginCommand::ToggleTab | PluginCommand::MovePane | PluginCommand::MovePaneWithDirection(..) | PluginCommand::MovePaneWithPaneId(..) | PluginCommand::MovePaneWithPaneIdInDirection(..) | PluginCommand::ClearScreen | PluginCommand::ClearScreenForPaneId(..) | PluginCommand::ScrollUp | PluginCommand::ScrollUpInPaneId(..) | PluginCommand::ScrollDown | PluginCommand::ScrollDownInPaneId(..) | PluginCommand::ScrollToTop | PluginCommand::ScrollToTopInPaneId(..) | PluginCommand::ScrollToBottom | PluginCommand::ScrollToBottomInPaneId(..) | PluginCommand::PageScrollUp | PluginCommand::PageScrollUpInPaneId(..) | PluginCommand::PageScrollDown | PluginCommand::PageScrollDownInPaneId(..) | PluginCommand::ToggleFocusFullscreen | PluginCommand::ToggleFocusNoUiFullscreen | PluginCommand::FocusHostSession | PluginCommand::TogglePaneIdFullscreen(..) | PluginCommand::TogglePaneFrames | PluginCommand::SetPaneFrameStyle(..) | PluginCommand::ToggleFloatingPanes { .. } | PluginCommand::NewPane | PluginCommand::TogglePaneEmbedOrEject | PluginCommand::TogglePaneEmbedOrEjectForPaneId(..) | PluginCommand::UndoRenamePane | PluginCommand::CloseFocus | PluginCommand::ToggleActiveTabSync | PluginCommand::CloseFocusedTab | PluginCommand::UndoRenameTab | PluginCommand::QuitZellij | PluginCommand::PreviousSwapLayout | PluginCommand::NextSwapLayout | PluginCommand::GoToTabName(..) | PluginCommand::FocusOrCreateTab(..) | PluginCommand::GoToTab(..) | PluginCommand::CloseTerminalPane(..) | PluginCommand::ClosePluginPane(..) | PluginCommand::FocusTerminalPane(..) | PluginCommand::FocusPluginPane(..) | PluginCommand::RenameTerminalPane(..) | PluginCommand::RenamePluginPane(..) | PluginCommand::SwitchSession(..) | PluginCommand::DeleteDeadSession(..) | PluginCommand::DeleteAllDeadSessions | PluginCommand::RenameSession(..) | PluginCommand::RenameTab(..) | PluginCommand::DisconnectOtherClients | PluginCommand::ShowPaneWithId(..) | PluginCommand::HidePaneWithId(..) | PluginCommand::RerunCommandPane(..) | PluginCommand::ResizePaneIdWithDirection(..) | PluginCommand::CloseTabWithIndex(..) | PluginCommand::BreakPanesToNewTab(..) | PluginCommand::BreakPanesToTabWithIndex(..) | PluginCommand::SwitchTabToId(..) | PluginCommand::GoToTabWithId(..) | PluginCommand::CloseTabWithId(..) | PluginCommand::RenameTabWithId(..) | PluginCommand::BreakPanesToTabWithId(..) | PluginCommand::ReloadPlugin(..) | PluginCommand::LoadNewPlugin { .. } | PluginCommand::SetFloatingPanePinned(..) | PluginCommand::StackPanes(..) | PluginCommand::ChangeFloatingPanesCoordinates(..) | PluginCommand::TogglePaneBorderless(..) | PluginCommand::SetPaneBorderless(..) | PluginCommand::SetPaneColor(..) | PluginCommand::GroupAndUngroupPanes(..) | PluginCommand::HighlightAndUnhighlightPanes(..) | PluginCommand::CloseMultiplePanes(..) | PluginCommand::FloatMultiplePanes(..) | PluginCommand::EmbedMultiplePanes(..) | PluginCommand::ReplacePaneWithExistingPane(..) | PluginCommand::KillSessions(..) | PluginCommand::KillSessionsAndReply(..) | PluginCommand::DeleteDeadSessionAndReply(..) | PluginCommand::DeleteAllDeadSessionsAndReply | PluginCommand::SendSigintToPaneId(..) | PluginCommand::SendSigkillToPaneId(..) | PluginCommand::OverrideLayout(..) | PluginCommand::SaveLayout { .. } | PluginCommand::DeleteLayout { .. } | PluginCommand::RenameLayout { .. } | PluginCommand::EditLayout { .. } | PluginCommand::ShowFloatingPanes { .. } | PluginCommand::HideFloatingPanes { .. } | PluginCommand::SetPaneRegexHighlights(..) | PluginCommand::ClearPaneHighlights(..) | PluginCommand::SetSoftKeyboard(..) | PluginCommand::SetTabFit { .. } | PluginCommand::ExitMobileMode => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, PluginCommand::ListClients | PluginCommand::DumpSessionLayout { .. } | PluginCommand::GetPanePid { .. } | PluginCommand::GetPaneRunningCommand { .. } | PluginCommand::GetPaneCwd { .. } | PluginCommand::GenerateRandomName | PluginCommand::GetLayoutDir | PluginCommand::GetFocusedPaneInfo | PluginCommand::DumpLayout(..) | PluginCommand::ParseLayout(..) | PluginCommand::SaveSession | PluginCommand::CurrentSessionLastSavedTime | PluginCommand::GetPaneInfo(..) | PluginCommand::GetTabInfo(..) | PluginCommand::GetSessionList => PermissionType::ReadApplicationState, PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => { PermissionType::Reconfigure }, PluginCommand::ChangeHostFolder(..) | PluginCommand::ListWindowsVolumes => { PermissionType::FullHdAccess }, PluginCommand::ShareCurrentSession | PluginCommand::StopSharingCurrentSession | PluginCommand::StopWebServer | PluginCommand::QueryWebServerStatus | PluginCommand::GenerateWebLoginToken(..) | PluginCommand::RevokeWebLoginToken(..) | PluginCommand::RevokeAllWebLoginTokens | PluginCommand::RenameWebLoginToken(..) | PluginCommand::ListWebLoginTokens | PluginCommand::StartWebServer => PermissionType::StartWebServer, PluginCommand::InterceptKeyPresses | PluginCommand::ClearKeyPressesIntercepts => { PermissionType::InterceptInput }, PluginCommand::GetPaneScrollback { .. } => PermissionType::ReadPaneContents, PluginCommand::RunAction(..) => PermissionType::RunActionsAsUser, PluginCommand::GetSessionEnvironmentVariables => { PermissionType::ReadSessionEnvironmentVariables }, PluginCommand::OpenCommandPaneInNewTab(..) => PermissionType::RunCommands, PluginCommand::OpenEditorPaneInNewTab(..) => PermissionType::OpenFiles, _ => return (PermissionStatus::Granted, None), }; if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() { if permissions.contains(&permission) { return (PermissionStatus::Granted, None); } } (PermissionStatus::Denied, Some(permission)) }