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, DeleteLayoutResponse, EditLayoutResponse, Event,
    FloatingPaneCoordinates, FocusOrCreateTabResponse, GetFocusedPaneInfoResponse,
    GetPaneCwdResponse, GetPanePidResponse, GetPaneRunningCommandResponse, HttpVerb,
    KeyWithModifier, LayoutInfo, LayoutMetadata, LayoutParsingError, MessageToPlugin,
    NewPanePlacement, NewTabResponse, 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, 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 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, ProtobufDeleteLayoutResponse,
            ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse,
            ProtobufEditLayoutResponse, ProtobufFocusOrCreateTabResponse,
            ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
            ProtobufGetLayoutDirResponse, ProtobufGetPaneCwdResponse, ProtobufGetPaneInfoResponse,
            ProtobufGetPanePidResponse, ProtobufGetPaneRunningCommandResponse,
            ProtobufGetSessionEnvironmentVariablesResponse, ProtobufGetSessionListResponse,
            ProtobufGetTabInfoResponse, ProtobufHideFloatingPanesResponse, ProtobufNewTabResponse,
            ProtobufNewTabsResponse, 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.capabilities.clone(),
            $env.client_attributes.clone(),
            $env.default_shell.clone(),
            &$env.default_layout,
            None,
            &$env.keybinds,
            $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<PluginEnv>) {
    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::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::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::TogglePaneFrames => toggle_pane_frames(env),
                    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::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::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<EventType>) -> 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<EventType>) -> 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<PermissionType>) -> 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<ProtobufEnvVariable> = 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<String, String>,
) {
    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<String, String>,
    context: BTreeMap<String, String>,
) {
    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<String, String>,
    floating_pane_coordinates: Option<FloatingPaneCoordinates>,
    context: BTreeMap<String, String>,
) {
    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,
        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<String, String>,
) {
    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<String, String>) {
    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,
        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<String, String>) {
    // 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 capabilities = env.capabilities.clone();
    let client_attributes = env.client_attributes.clone();
    let default_shell = env.default_shell.clone();
    let default_layout = env.default_layout.clone();
    let keybinds = env.keybinds.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(),
            capabilities,
            client_attributes,
            default_shell,
            &default_layout,
            None,
            &keybinds,
            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<FloatingPaneCoordinates>,
    context: BTreeMap<String, String>,
) {
    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,
        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<String, String>,
) {
    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,
        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<String, String>,
) {
    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
    );
    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<FloatingPaneCoordinates>,
    context: BTreeMap<String, String>,
) {
    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
    );
    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<String, String>,
) {
    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<RunCommandAction> = match default_shell {
        TerminalAction::RunCommand(run_command) => Some(run_command.into()),
        _ => None,
    };
    let action = Action::NewTiledPane {
        direction: None,
        command: run_command_action,
        pane_name: None,
        near_current_pane: 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
    ));

    // 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<FloatingPaneCoordinates>,
) {
    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<RunCommandAction> = match default_shell {
        TerminalAction::RunCommand(run_command) => Some(run_command.into()),
        _ => None,
    };
    let action = Action::NewFloatingPane {
        command: run_command_action,
        pane_name: None,
        coordinates: floating_pane_coordinates,
        near_current_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: 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<FloatingPaneCoordinates>,
) {
    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
    ));

    // 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<RunCommandAction> = 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,
        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<String, String>,
) {
    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<String, String>,
) {
    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<String, String>,
) {
    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<String, String>,
) {
    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 {
        direction,
        command: Some(run_command_action),
        pane_name: name,
        near_current_pane: 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<String, String>,
) {
    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
    ));

    // 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<FloatingPaneCoordinates>,
    context: BTreeMap<String, String>,
) {
    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 {
        command: Some(run_command_action),
        pane_name: name,
        coordinates: floating_pane_coordinates,
        near_current_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: 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<FloatingPaneCoordinates>,
    context: BTreeMap<String, String>,
) {
    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
    ));

    // 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<String, String>,
) {
    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,
        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<String, String>,
) {
    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
    ));

    // 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<String>) {
    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<String>,
    env_variables: BTreeMap<String, String>,
    cwd: PathBuf,
    context: BTreeMap<String, String>,
) {
    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<String, String>,
    body: Vec<u8>,
    context: BTreeMap<String, String>,
) {
    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<Action>)>,
    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<String>, cwd: Option<String>) {
    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 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 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<String>,
    tab_position: Option<usize>,
    pane_id: Option<(u32, bool)>,
    layout: Option<LayoutInfo>,
    cwd: Option<PathBuf>,
) -> 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<String> = 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<u8>) {
    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_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 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 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<String>) {
    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::<ClientToServerMsg>::new(stream)
                        .send_client_msg(ClientToServerMsg::KillSession);
                    if let Ok(reply_stream) = reply {
                        let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
                            IpcReceiverWithContext::new(reply_stream);
                        let _ = receiver.recv_server_msg();
                    }
                }
                #[cfg(not(windows))]
                {
                    let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
                        .send_client_msg(ClientToServerMsg::KillSession);
                }
            },
            Err(e) => {
                log::error!("Failed to kill session {}: {:?}", session_name, e);
            },
        };
    }
}

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<usize>) {
    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<usize>) {
    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<usize>) {
    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<RegexHighlight>,
) {
    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<PaneId>) {
    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<String>, bg: Option<String>) {
    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");
}

#[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<zellij_utils::data::FileMetadata>)> {
    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<zellij_utils::data::FileMetadata>)> {
    // 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<u16> = 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<u8>, 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<GetPaneRunningCommandResponse>,
) {
    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>,
) -> 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<GetPaneCwdResponse>,
) {
    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>,
) -> 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<String, String>) {
    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<String, String>,
) -> 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,
        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.capabilities.clone(),
        env.client_attributes.clone(),
        env.default_shell.clone(),
        &env.default_layout,
        None,
        &env.keybinds,
        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<String, String> {
    // 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<PaneId>,
    new_tab_name: Option<String>,
    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<PaneId>,
    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<PaneId>,
    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<String, String>,
    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<PaneId>,
    panes_to_ungroup: Vec<PaneId>,
    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<PaneId>,
    panes_to_unhighlight: Vec<PaneId>,
) {
    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<PaneId>) {
    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<PaneId>) {
    let _ = env
        .senders
        .send_to_screen(ScreenInstruction::FloatMultiplePanes(
            pane_ids,
            env.client_id,
        ));
}

fn embed_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
    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<String>, 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<String>, _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<String, String>,
) -> 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<TabLayoutInfo>
    let tabs: Vec<TabLayoutInfo> = 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<String> {
    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<Vec<u8>> {
    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<PermissionType>) {
    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::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::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::TogglePaneIdFullscreen(..)
        | PluginCommand::TogglePaneFrames
        | 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::SendSigintToPaneId(..)
        | PluginCommand::SendSigkillToPaneId(..)
        | PluginCommand::OverrideLayout(..)
        | PluginCommand::SaveLayout { .. }
        | PluginCommand::DeleteLayout { .. }
        | PluginCommand::RenameLayout { .. }
        | PluginCommand::EditLayout { .. }
        | PluginCommand::ShowFloatingPanes { .. }
        | PluginCommand::HideFloatingPanes { .. }
        | PluginCommand::SetPaneRegexHighlights(..)
        | PluginCommand::ClearPaneHighlights(..) => 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(..) | PluginCommand::OpenEditorPaneInNewTab(..) => {
            PermissionType::ChangeApplicationState
        },
        _ => 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))
}
