diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index fe8b034b..811a4507 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -39,6 +39,16 @@ register_worker!(TestWorker, test_worker, TEST_WORKER); impl ZellijPlugin for State { fn load(&mut self, configuration: BTreeMap) { + request_permission(&[ + PermissionType::ChangeApplicationState, + PermissionType::ReadApplicationState, + PermissionType::ReadApplicationState, + PermissionType::ChangeApplicationState, + PermissionType::OpenFiles, + PermissionType::RunCommands, + PermissionType::OpenTerminalsOrPlugins, + PermissionType::WriteToStdin, + ]); self.configuration = configuration; subscribe(&[ EventType::InputReceived, @@ -227,6 +237,9 @@ impl ZellijPlugin for State { Key::Ctrl('z') => { go_to_tab_name(&format!("{:?}", self.configuration)); }, + Key::Ctrl('1') => { + request_permission(&[PermissionType::ReadApplicationState]); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index e620930c..3d4c94a9 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -30,6 +30,7 @@ impl ZellijPlugin for State { EventType::FileSystemCreate, EventType::FileSystemUpdate, EventType::FileSystemDelete, + EventType::PermissionRequestResult, ]); post_message_to(PluginMessage { worker_name: Some("file_name_search".into()), @@ -54,6 +55,9 @@ impl ZellijPlugin for State { }; self.ev_history.push_back((event.clone(), Instant::now())); match event { + Event::PermissionRequestResult(_) => { + should_render = true; + }, Event::Timer(_elapsed) => { if self.search_state.loading { set_timeout(0.5); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 2de5b74e..1f2d770b 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -295,7 +295,7 @@ impl FloatingPanes { pane.render_full_viewport(); } } - pub fn set_pane_frames(&mut self, os_api: &mut Box) -> Result<()> { + pub fn set_pane_frames(&mut self, _os_api: &mut Box) -> Result<()> { let err_context = |pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}"); @@ -392,7 +392,7 @@ impl FloatingPanes { self.set_force_render(); } - pub fn resize_pty_all_panes(&mut self, os_api: &mut Box) -> Result<()> { + pub fn resize_pty_all_panes(&mut self, _os_api: &mut Box) -> Result<()> { for pane in self.panes.values_mut() { resize_pty!(pane, os_api, self.senders, self.character_cell_size) .with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?; @@ -403,7 +403,7 @@ impl FloatingPanes { pub fn resize_active_pane( &mut self, client_id: ClientId, - os_api: &mut Box, + _os_api: &mut Box, strategy: &ResizeStrategy, ) -> Result { // true => successfully resized @@ -838,7 +838,7 @@ impl FloatingPanes { self.focus_pane_for_all_clients(focused_pane); } } - pub fn switch_active_pane_with(&mut self, os_api: &mut Box, pane_id: PaneId) { + pub fn switch_active_pane_with(&mut self, _os_api: &mut Box, pane_id: PaneId) { if let Some(active_pane_id) = self.first_active_floating_pane_id() { let current_position = self.panes.get(&active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 46552960..b4e3461e 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::time::Instant; use crate::output::{CharacterChunk, SixelImageChunk}; use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId}; use crate::plugins::PluginInstruction; use crate::pty::VteBytes; -use crate::tab::Pane; +use crate::tab::{AdjustedInput, Pane}; use crate::ui::{ loading_indication::LoadingIndication, pane_boundaries_frame::{FrameParams, PaneFrame}, @@ -13,6 +13,7 @@ use crate::ui::{ use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; +use zellij_utils::data::{PermissionStatus, PermissionType, PluginPermission}; use zellij_utils::pane_size::{Offset, SizeInPixels}; use zellij_utils::position::Position; use zellij_utils::{ @@ -25,6 +26,15 @@ use zellij_utils::{ vte, }; +macro_rules! style { + ($fg:expr) => { + ansi_term::Style::new().fg(match $fg { + PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b), + PaletteColor::EightBit(color) => ansi_term::Color::Fixed(color), + }) + }; +} + macro_rules! get_or_create_grid { ($self:ident, $client_id:ident) => {{ let rows = $self.get_content_rows(); @@ -73,6 +83,7 @@ pub(crate) struct PluginPane { pane_frame_color_override: Option<(PaletteColor, Option)>, invoked_with: Option, loading_indication: LoadingIndication, + requesting_permissions: Option, debug: bool, } @@ -121,6 +132,7 @@ impl PluginPane { pane_frame_color_override: None, invoked_with, loading_indication, + requesting_permissions: None, debug, }; for client_id in currently_connected_clients { @@ -181,6 +193,14 @@ impl Pane for PluginPane { } fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) { self.set_client_should_render(client_id, true); + + let mut vte_bytes = bytes; + if let Some(plugin_permission) = &self.requesting_permissions { + vte_bytes = self + .display_request_permission_message(plugin_permission) + .into(); + } + let grid = get_or_create_grid!(self, client_id); // this is part of the plugin contract, whenever we update the plugin and call its render function, we delete the existing viewport @@ -193,14 +213,36 @@ impl Pane for PluginPane { .vte_parsers .entry(client_id) .or_insert_with(|| vte::Parser::new()); - for &byte in &bytes { + + for &byte in &vte_bytes { vte_parser.advance(grid, byte); } + self.should_render.insert(client_id, true); } fn cursor_coordinates(&self) -> Option<(usize, usize)> { None } + fn adjust_input_to_terminal(&mut self, input_bytes: Vec) -> Option { + if let Some(requesting_permissions) = &self.requesting_permissions { + let permissions = requesting_permissions.permissions.clone(); + match input_bytes.as_slice() { + // Y or y + &[89] | &[121] => Some(AdjustedInput::PermissionRequestResult( + permissions, + PermissionStatus::Granted, + )), + // N or n + &[78] | &[110] => Some(AdjustedInput::PermissionRequestResult( + permissions, + PermissionStatus::Denied, + )), + _ => None, + } + } else { + Some(AdjustedInput::WriteBytesToTerminal(input_bytes)) + } + } fn position_and_size(&self) -> PaneGeom { self.geom } @@ -233,6 +275,9 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } + fn request_permissions_from_user(&mut self, permissions: Option) { + self.requesting_permissions = permissions; + } fn render( &mut self, client_id: Option, @@ -595,4 +640,54 @@ impl PluginPane { self.handle_plugin_bytes(client_id, bytes.clone()); } } + fn display_request_permission_message(&self, plugin_permission: &PluginPermission) -> String { + let bold_white = style!(self.style.colors.white).bold(); + let cyan = style!(self.style.colors.cyan).bold(); + let orange = style!(self.style.colors.orange).bold(); + let green = style!(self.style.colors.green).bold(); + + let mut messages = String::new(); + let permissions: BTreeSet = + plugin_permission.permissions.clone().into_iter().collect(); + + let min_row_count = permissions.len() + 4; + + if self.rows() >= min_row_count { + messages.push_str(&format!( + "{} {} {}\n", + bold_white.paint("Plugin"), + cyan.paint(&plugin_permission.name), + bold_white.paint("asks permission to:"), + )); + permissions.iter().enumerate().for_each(|(i, p)| { + messages.push_str(&format!( + "\n\r{}. {}", + bold_white.paint(&format!("{}", i + 1)), + orange.paint(p.display_name()) + )); + }); + + messages.push_str(&format!( + "\n\n\r{} {}", + bold_white.paint("Allow?"), + green.paint("(y/n)"), + )); + } else { + messages.push_str(&format!( + "{} {}. {} {}\n", + bold_white.paint("This plugin asks permission to:"), + orange.paint( + permissions + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(", ") + ), + bold_white.paint("Allow?"), + green.paint("(y/n)"), + )); + } + + messages + } } diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 9aca1af4..736c16bc 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -68,7 +68,6 @@ pub struct TiledPanes { draw_pane_frames: bool, panes_to_hide: HashSet, fullscreen_is_active: bool, - os_api: Box, senders: ThreadSenders, window_title: Option, client_id_to_boundaries: HashMap, @@ -105,7 +104,6 @@ impl TiledPanes { draw_pane_frames, panes_to_hide: HashSet::new(), fullscreen_is_active: false, - os_api, senders, window_title: None, client_id_to_boundaries: HashMap::new(), diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index f36bdc2d..4bef0683 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -18,7 +18,7 @@ use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction}; use wasm_bridge::WasmBridge; use zellij_utils::{ - data::{Event, EventType, PluginCapabilities}, + data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities}, errors::{prelude::*, ContextType, PluginContext}, input::{ command::TerminalAction, @@ -79,6 +79,13 @@ pub enum PluginInstruction { String, // serialized payload ), PluginSubscribedToEvents(PluginId, ClientId, HashSet), + PermissionRequestResult( + PluginId, + Option, + Vec, + PermissionStatus, + Option, + ), Exit, } @@ -105,6 +112,9 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::PluginSubscribedToEvents(..) => { PluginContext::PluginSubscribedToEvents }, + PluginInstruction::PermissionRequestResult(..) => { + PluginContext::PermissionRequestResult + }, } } } @@ -287,6 +297,30 @@ pub(crate) fn plugin_thread_main( } } }, + PluginInstruction::PermissionRequestResult( + plugin_id, + client_id, + permissions, + status, + cache_path, + ) => { + if let Err(e) = wasm_bridge.cache_plugin_permissions( + plugin_id, + client_id, + permissions, + status, + cache_path, + ) { + log::error!("{}", e); + } + + let updates = vec![( + Some(plugin_id), + client_id, + Event::PermissionRequestResult(status), + )]; + wasm_bridge.update_plugins(updates)?; + }, PluginInstruction::Exit => { wasm_bridge.cleanup(); break; diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 92a4c048..ea0c2b6a 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -188,6 +188,7 @@ impl<'a> PluginLoader<'a> { display_loading_stage!(end, loading_indication, senders, plugin_id); Ok(()) } + pub fn add_client( client_id: ClientId, plugin_dir: PathBuf, @@ -613,6 +614,19 @@ impl<'a> PluginLoader<'a> { } start_function.call(&[]).with_context(err_context)?; + plugin_map.lock().unwrap().insert( + self.plugin_id, + self.client_id, + Arc::new(Mutex::new(RunningPlugin::new( + main_user_instance, + main_user_env, + self.size.rows, + self.size.cols, + ))), + subscriptions.clone(), + workers, + ); + let protobuf_plugin_configuration: ProtobufPluginConfiguration = self .plugin .userspace_configuration @@ -640,18 +654,6 @@ impl<'a> PluginLoader<'a> { self.senders, self.plugin_id ); - plugin_map.lock().unwrap().insert( - self.plugin_id, - self.client_id, - Arc::new(Mutex::new(RunningPlugin::new( - main_user_instance, - main_user_env, - self.size.rows, - self.size.cols, - ))), - subscriptions.clone(), - workers, - ); display_loading_stage!( indicate_writing_plugin_to_cache_success, self.loading_indication, @@ -764,13 +766,13 @@ impl<'a> PluginLoader<'a> { }) .with_context(err_context)?; let wasi = wasi_env.import_object(&module).with_context(err_context)?; - let mut mut_plugin = self.plugin.clone(); mut_plugin.set_tab_index(self.tab_index); let plugin_env = PluginEnv { plugin_id: self.plugin_id, client_id: self.client_id, plugin: mut_plugin, + permissions: Arc::new(Mutex::new(None)), senders: self.senders.clone(), wasi_env, plugin_own_data_dir: self.plugin_own_data_dir.clone(), diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index 2f7454c7..0cf26943 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -11,7 +11,6 @@ use wasmer_wasi::WasiEnv; use crate::{thread_bus::ThreadSenders, ClientId}; use zellij_utils::async_channel::Sender; -use zellij_utils::errors::prelude::*; use zellij_utils::{ data::EventType, data::PluginCapabilities, @@ -20,6 +19,7 @@ use zellij_utils::{ input::plugins::PluginConfig, ipc::ClientAttributes, }; +use zellij_utils::{data::PermissionType, errors::prelude::*}; // the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new // client connects) but to also allow updates/renders not to block each other @@ -193,6 +193,7 @@ pub type Subscriptions = HashSet; pub struct PluginEnv { pub plugin_id: PluginId, pub plugin: PluginConfig, + pub permissions: Arc>>>, pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub tab_index: usize, @@ -215,6 +216,10 @@ impl PluginEnv { self.plugin_id ) } + + pub fn set_permissions(&mut self, permissions: HashSet) { + self.permissions.lock().unwrap().replace(permissions); + } } #[derive(Eq, PartialEq, Hash)] diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 663ca94e..e995a07b 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -6,9 +6,10 @@ use std::collections::BTreeMap; use std::path::PathBuf; use tempfile::tempdir; use wasmer::Store; -use zellij_utils::data::{Event, Key, PluginCapabilities}; +use zellij_utils::data::{Event, Key, PermissionStatus, PermissionType, PluginCapabilities}; use zellij_utils::errors::ErrorContext; use zellij_utils::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; +use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::plugins::PluginsConfig; use zellij_utils::ipc::ClientAttributes; use zellij_utils::lazy_static::lazy_static; @@ -52,6 +53,157 @@ macro_rules! log_actions_in_thread { }; } +macro_rules! grant_permissions_and_log_actions_in_thread { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { + std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + let cache_path = $cache_path.clone(); + let plugin_thread_sender = $plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event(..) => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + if plugin_permission.permissions.contains($permission_type) { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Granted, + Some(cache_path.clone()), + ), + ); + } else { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Denied, + Some(cache_path.clone()), + ), + ); + } + }, + _ => { + log.lock().unwrap().push(event); + }, + } + } + }) + .unwrap() + }; +} + +macro_rules! deny_permissions_and_log_actions_in_thread { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { + std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + let cache_path = $cache_path.clone(); + let plugin_thread_sender = $plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event(..) => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Denied, + Some(cache_path.clone()), + ), + ); + break; + }, + _ => { + log.lock().unwrap().push(event); + }, + } + } + }) + .unwrap() + }; +} + +macro_rules! grant_permissions_and_log_actions_in_thread_naked_variant { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { + std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + let cache_path = $cache_path.clone(); + let plugin_thread_sender = $plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + if plugin_permission.permissions.contains($permission_type) { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Granted, + Some(cache_path.clone()), + ), + ); + } else { + let _ = plugin_thread_sender.send( + PluginInstruction::PermissionRequestResult( + 0, + Some($client_id), + plugin_permission.permissions, + PermissionStatus::Denied, + Some(cache_path.clone()), + ), + ); + } + }, + _ => { + log.lock().unwrap().push(event); + }, + } + } + }) + .unwrap() + }; +} + macro_rules! log_actions_in_thread_naked_variant { ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr ) => { std::thread::Builder::new() @@ -167,14 +319,14 @@ fn create_plugin_thread_with_server_receiver( ) -> ( SenderWithContext, Receiver<(ServerInstruction, ErrorContext)>, + Receiver<(ScreenInstruction, ErrorContext)>, Box, ) { let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); let (to_server, server_receiver): ChannelWithContext = channels::bounded(50); let to_server = SenderWithContext::new(to_server); - let (to_screen, _screen_receiver): ChannelWithContext = - channels::unbounded(); + let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); let to_screen = SenderWithContext::new(to_screen); let (to_plugin, plugin_receiver): ChannelWithContext = channels::unbounded(); @@ -240,7 +392,12 @@ fn create_plugin_thread_with_server_receiver( // the plugin cache } }; - (to_plugin, server_receiver, Box::new(teardown)) + ( + to_plugin, + server_receiver, + screen_receiver, + Box::new(teardown), + ) } fn create_plugin_thread_with_pty_receiver( @@ -248,6 +405,7 @@ fn create_plugin_thread_with_pty_receiver( ) -> ( SenderWithContext, Receiver<(PtyInstruction, ErrorContext)>, + Receiver<(ScreenInstruction, ErrorContext)>, Box, ) { let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); @@ -255,8 +413,7 @@ fn create_plugin_thread_with_pty_receiver( channels::bounded(50); let to_server = SenderWithContext::new(to_server); - let (to_screen, _screen_receiver): ChannelWithContext = - channels::unbounded(); + let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); let to_screen = SenderWithContext::new(to_screen); let (to_plugin, plugin_receiver): ChannelWithContext = channels::unbounded(); @@ -322,7 +479,7 @@ fn create_plugin_thread_with_pty_receiver( // the plugin cache } }; - (to_plugin, pty_receiver, Box::new(teardown)) + (to_plugin, pty_receiver, screen_receiver, Box::new(teardown)) } lazy_static! { @@ -344,6 +501,9 @@ pub fn load_new_plugin_from_hd() { // message (this is what the fixture plugin does) // we then listen on our mock screen receiver to make sure we got a PluginBytes instruction // that contains said render, and assert against it + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -359,11 +519,15 @@ pub fn load_new_plugin_from_hd() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PluginBytes, screen_receiver, - 2 + 2, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -403,8 +567,12 @@ pub fn load_new_plugin_from_hd() { #[test] #[ignore] pub fn plugin_workers() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { _allow_exec_host_cmd: false, @@ -418,11 +586,15 @@ pub fn plugin_workers() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PluginBytes, screen_receiver, - 3 + 3, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -465,8 +637,12 @@ pub fn plugin_workers() { #[test] #[ignore] pub fn plugin_workers_persist_state() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { _allow_exec_host_cmd: false, @@ -480,11 +656,15 @@ pub fn plugin_workers_persist_state() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PluginBytes, screen_receiver, - 5 + 5, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -603,6 +783,7 @@ pub fn switch_to_mode_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -619,11 +800,82 @@ pub fn switch_to_mode_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ChangeMode, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Char('a')), // this triggers a SwitchToMode(Tab) command in the fixture + // plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let switch_to_mode_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ChangeMode(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", switch_to_mode_event)); +} + +#[test] +#[ignore] +pub fn switch_to_mode_plugin_command_permission_denied() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = deny_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ChangeMode, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -665,6 +917,7 @@ pub fn new_tabs_with_layout_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -681,11 +934,15 @@ pub fn new_tabs_with_layout_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::NewTab, screen_receiver, - 2 + 2, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -741,6 +998,7 @@ pub fn new_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -757,11 +1015,15 @@ pub fn new_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::NewTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -803,6 +1065,7 @@ pub fn go_to_next_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -819,11 +1082,15 @@ pub fn go_to_next_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::SwitchTabNext, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -864,6 +1131,7 @@ pub fn go_to_previous_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -880,11 +1148,15 @@ pub fn go_to_previous_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::SwitchTabPrev, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -925,6 +1197,7 @@ pub fn resize_focused_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -941,11 +1214,15 @@ pub fn resize_focused_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::Resize, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -986,6 +1263,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1002,11 +1280,15 @@ pub fn resize_focused_pane_with_direction_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::Resize, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1047,6 +1329,7 @@ pub fn focus_next_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1063,11 +1346,15 @@ pub fn focus_next_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::FocusNextPane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1108,6 +1395,7 @@ pub fn focus_previous_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1124,11 +1412,15 @@ pub fn focus_previous_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::FocusPreviousPane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1169,6 +1461,7 @@ pub fn move_focus_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1185,11 +1478,15 @@ pub fn move_focus_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::MoveFocusLeft, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1230,6 +1527,7 @@ pub fn move_focus_or_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1246,11 +1544,15 @@ pub fn move_focus_or_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::MoveFocusLeftOrPreviousTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1291,6 +1593,7 @@ pub fn edit_scrollback_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1307,11 +1610,15 @@ pub fn edit_scrollback_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::EditScrollback, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1352,6 +1659,7 @@ pub fn write_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1368,11 +1676,15 @@ pub fn write_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::WriteCharacter, screen_receiver, - 1 + 1, + &PermissionType::WriteToStdin, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1413,6 +1725,7 @@ pub fn write_chars_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1429,11 +1742,15 @@ pub fn write_chars_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::WriteCharacter, screen_receiver, - 1 + 1, + &PermissionType::WriteToStdin, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1474,6 +1791,7 @@ pub fn toggle_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1490,11 +1808,15 @@ pub fn toggle_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ToggleTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1535,6 +1857,7 @@ pub fn move_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1551,11 +1874,15 @@ pub fn move_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::MovePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1596,6 +1923,7 @@ pub fn move_pane_with_direction_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1612,11 +1940,15 @@ pub fn move_pane_with_direction_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::MovePaneLeft, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1657,6 +1989,7 @@ pub fn clear_screen_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1673,11 +2006,16 @@ pub fn clear_screen_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ClearScreen, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1689,6 +2027,7 @@ pub fn clear_screen_plugin_command() { client_id, size, )); + std::thread::sleep(std::time::Duration::from_millis(100)); let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( None, Some(client_id), @@ -1718,6 +2057,7 @@ pub fn scroll_up_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1734,11 +2074,16 @@ pub fn scroll_up_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ScrollUp, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1779,6 +2124,7 @@ pub fn scroll_down_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1795,11 +2141,15 @@ pub fn scroll_down_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ScrollDown, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1840,6 +2190,7 @@ pub fn scroll_to_top_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1856,11 +2207,15 @@ pub fn scroll_to_top_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ScrollToTop, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1901,6 +2256,7 @@ pub fn scroll_to_bottom_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1917,11 +2273,15 @@ pub fn scroll_to_bottom_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ScrollToBottom, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -1962,6 +2322,7 @@ pub fn page_scroll_up_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -1978,11 +2339,15 @@ pub fn page_scroll_up_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PageScrollUp, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2023,6 +2388,7 @@ pub fn page_scroll_down_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2039,11 +2405,15 @@ pub fn page_scroll_down_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PageScrollDown, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2084,6 +2454,7 @@ pub fn toggle_focus_fullscreen_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2100,11 +2471,15 @@ pub fn toggle_focus_fullscreen_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ToggleActiveTerminalFullscreen, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2145,6 +2520,7 @@ pub fn toggle_pane_frames_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2161,11 +2537,15 @@ pub fn toggle_pane_frames_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread_naked_variant!( + let screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( received_screen_instructions, ScreenInstruction::TogglePaneFrames, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2206,6 +2586,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2222,11 +2603,15 @@ pub fn toggle_pane_embed_or_eject_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::TogglePaneEmbedOrFloating, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2267,6 +2652,7 @@ pub fn undo_rename_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2283,11 +2669,15 @@ pub fn undo_rename_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::UndoRenamePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2328,6 +2718,7 @@ pub fn close_focus_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2344,11 +2735,15 @@ pub fn close_focus_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::CloseFocusedPane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2389,6 +2784,7 @@ pub fn toggle_active_tab_sync_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2405,11 +2801,15 @@ pub fn toggle_active_tab_sync_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ToggleActiveSyncTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2450,6 +2850,7 @@ pub fn close_focused_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2466,11 +2867,15 @@ pub fn close_focused_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::CloseTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2511,6 +2916,7 @@ pub fn undo_rename_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2527,11 +2933,15 @@ pub fn undo_rename_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::UndoRenameTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2572,6 +2982,7 @@ pub fn previous_swap_layout_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2588,11 +2999,15 @@ pub fn previous_swap_layout_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::PreviousSwapLayout, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2633,6 +3048,7 @@ pub fn next_swap_layout_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2649,11 +3065,15 @@ pub fn next_swap_layout_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::NextSwapLayout, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2694,6 +3114,7 @@ pub fn go_to_tab_name_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2710,11 +3131,15 @@ pub fn go_to_tab_name_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::GoToTabName, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2755,6 +3180,7 @@ pub fn focus_or_create_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2771,11 +3197,15 @@ pub fn focus_or_create_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::GoToTabName, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2816,6 +3246,7 @@ pub fn go_to_tab() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2832,11 +3263,15 @@ pub fn go_to_tab() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::GoToTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2877,6 +3312,7 @@ pub fn start_or_reload_plugin() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -2893,11 +3329,15 @@ pub fn start_or_reload_plugin() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::StartOrReloadPluginPane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -2938,7 +3378,8 @@ pub fn quit_zellij_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, server_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, server_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_server_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -2954,12 +3395,23 @@ pub fn quit_zellij_plugin_command() { rows: 20, }; let received_server_instruction = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let server_thread = log_actions_in_thread!( received_server_instruction, ServerInstruction::ClientExit, server_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -2976,7 +3428,7 @@ pub fn quit_zellij_plugin_command() { Event::Key(Key::Char('8')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + server_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); let new_tab_event = received_server_instruction .lock() @@ -2999,7 +3451,8 @@ pub fn detach_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, server_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, server_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_server_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3015,12 +3468,23 @@ pub fn detach_plugin_command() { rows: 20, }; let received_server_instruction = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let server_thread = log_actions_in_thread!( received_server_instruction, ServerInstruction::DetachSession, server_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3037,7 +3501,7 @@ pub fn detach_plugin_command() { Event::Key(Key::Char('l')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + server_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); let new_tab_event = received_server_instruction .lock() @@ -3060,7 +3524,8 @@ pub fn open_file_floating_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3075,13 +3540,24 @@ pub fn open_file_floating_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3098,9 +3574,9 @@ pub fn open_file_floating_plugin_command() { Event::Key(Key::Ctrl('h')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3121,7 +3597,8 @@ pub fn open_file_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3136,13 +3613,24 @@ pub fn open_file_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3159,9 +3647,9 @@ pub fn open_file_plugin_command() { Event::Key(Key::Ctrl('g')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3182,7 +3670,8 @@ pub fn open_file_with_line_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3197,14 +3686,26 @@ pub fn open_file_with_line_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( plugin_should_float, @@ -3220,9 +3721,9 @@ pub fn open_file_with_line_plugin_command() { Event::Key(Key::Ctrl('i')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3243,7 +3744,8 @@ pub fn open_file_with_line_floating_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3258,13 +3760,24 @@ pub fn open_file_with_line_floating_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3281,9 +3794,9 @@ pub fn open_file_with_line_floating_plugin_command() { Event::Key(Key::Ctrl('j')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3304,7 +3817,8 @@ pub fn open_terminal_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3319,13 +3833,24 @@ pub fn open_terminal_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3342,9 +3867,9 @@ pub fn open_terminal_plugin_command() { Event::Key(Key::Ctrl('k')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3365,7 +3890,8 @@ pub fn open_terminal_floating_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3380,13 +3906,24 @@ pub fn open_terminal_floating_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3403,9 +3940,9 @@ pub fn open_terminal_floating_plugin_command() { Event::Key(Key::Ctrl('l')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3426,7 +3963,8 @@ pub fn open_command_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3441,13 +3979,24 @@ pub fn open_command_pane_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3464,9 +4013,9 @@ pub fn open_command_pane_plugin_command() { Event::Key(Key::Ctrl('m')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3487,7 +4036,8 @@ pub fn open_command_pane_floating_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); - let (plugin_thread_sender, pty_receiver, mut teardown) = + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) = create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); @@ -3502,13 +4052,24 @@ pub fn open_command_pane_floating_plugin_command() { cols: 121, rows: 20, }; - let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( - received_screen_instructions, + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let pty_thread = log_actions_in_thread!( + received_pty_instructions, PtyInstruction::SpawnTerminal, pty_receiver, 1 ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); let _ = plugin_thread_sender.send(PluginInstruction::Load( @@ -3525,9 +4086,9 @@ pub fn open_command_pane_floating_plugin_command() { Event::Key(Key::Ctrl('n')), // this triggers the enent in the fixture plugin )])); std::thread::sleep(std::time::Duration::from_millis(100)); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + pty_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let new_tab_event = received_screen_instructions + let new_tab_event = received_pty_instructions .lock() .unwrap() .iter() @@ -3548,6 +4109,7 @@ pub fn switch_to_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3564,11 +4126,15 @@ pub fn switch_to_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::GoToTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -3731,6 +4297,7 @@ pub fn close_terminal_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3747,11 +4314,15 @@ pub fn close_terminal_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ClosePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -3792,6 +4363,7 @@ pub fn close_plugin_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3808,11 +4380,15 @@ pub fn close_plugin_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::ClosePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -3853,6 +4429,7 @@ pub fn focus_terminal_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3869,11 +4446,15 @@ pub fn focus_terminal_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::FocusPaneWithId, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -3914,6 +4495,7 @@ pub fn focus_plugin_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3930,11 +4512,15 @@ pub fn focus_plugin_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::FocusPaneWithId, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -3975,6 +4561,7 @@ pub fn rename_terminal_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -3991,11 +4578,15 @@ pub fn rename_terminal_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::RenamePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -4036,6 +4627,7 @@ pub fn rename_plugin_pane_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -4052,11 +4644,15 @@ pub fn rename_plugin_pane_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::RenamePane, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -4097,6 +4693,7 @@ pub fn rename_tab_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -4113,11 +4710,15 @@ pub fn rename_tab_plugin_command() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::RenameTab, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -4158,6 +4759,7 @@ pub fn send_configuration_to_plugins() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(Some(plugin_host_folder)); let plugin_should_float = Some(false); @@ -4183,11 +4785,15 @@ pub fn send_configuration_to_plugins() { rows: 20, }; let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = log_actions_in_thread!( + let screen_thread = grant_permissions_and_log_actions_in_thread!( received_screen_instructions, ScreenInstruction::GoToTabName, screen_receiver, - 1 + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id ); let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); @@ -4224,3 +4830,226 @@ pub fn send_configuration_to_plugins() { .clone(); assert_snapshot!(format!("{:#?}", go_to_tab_event)); } + +#[test] +#[ignore] +pub fn request_plugin_permissions() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::RequestPluginPermissions, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::RequestPluginPermissions(_, plugin_permission) = i { + Some(plugin_permission.permissions.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn granted_permission_request_result() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + // here we create a fake screen thread that will send a PermissionStatus::Granted + // message for every permission request it gets + let screen_thread = std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let cache_path = cache_path.clone(); + let plugin_thread_sender = plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = screen_receiver + .recv() + .expect("failed to receive event on channel"); + match event { + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + let _ = + plugin_thread_sender.send(PluginInstruction::PermissionRequestResult( + 0, + Some(client_id), + plugin_permission.permissions, + PermissionStatus::Granted, + Some(cache_path.clone()), + )); + break; + }, + ScreenInstruction::Exit => { + break; + }, + _ => {}, + } + } + }) + .unwrap(); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin.clone(), + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin + )])); + screen_thread.join().unwrap(); + teardown(); + + let permission_cache = PermissionCache::from_path_or_default(Some(cache_path)); + let mut permissions = permission_cache + .get_permissions(run_plugin.location.to_string()) + .clone(); + let permissions = permissions.as_mut().map(|p| { + let mut permissions = p.clone(); + permissions.sort_unstable(); + permissions + }); + + assert_snapshot!(format!("{:#?}", permissions)); +} + +#[test] +#[ignore] +pub fn denied_permission_request_result() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + // here we create a fake screen thread that will send a PermissionStatus::Granted + // message for every permission request it gets + let screen_thread = std::thread::Builder::new() + .name("fake_screen_thread".to_string()) + .spawn({ + let cache_path = cache_path.clone(); + let plugin_thread_sender = plugin_thread_sender.clone(); + move || loop { + let (event, _err_ctx) = screen_receiver + .recv() + .expect("failed to receive event on channel"); + match event { + ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => { + let _ = + plugin_thread_sender.send(PluginInstruction::PermissionRequestResult( + 0, + Some(client_id), + plugin_permission.permissions, + PermissionStatus::Denied, + Some(cache_path.clone()), + )); + break; + }, + ScreenInstruction::Exit => { + break; + }, + _ => {}, + } + } + }) + .unwrap(); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin.clone(), + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin + )])); + screen_thread.join().unwrap(); + teardown(); + + let permission_cache = PermissionCache::from_path_or_default(Some(cache_path)); + let permissions = permission_cache.get_permissions(run_plugin.location.to_string()); + + assert_snapshot!(format!("{:#?}", permissions)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap new file mode 100644 index 00000000..d7dfeb16 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap @@ -0,0 +1,7 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", permissions)" +--- +Some( + [], +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap new file mode 100644 index 00000000..beaa4adc --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap @@ -0,0 +1,15 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 4864 +expression: "format!(\"{:#?}\", permissions)" +--- +Some( + [ + ReadApplicationState, + ChangeApplicationState, + OpenFiles, + RunCommands, + OpenTerminalsOrPlugins, + WriteToStdin, + ], +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap new file mode 100644 index 00000000..23fa6c22 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap @@ -0,0 +1,17 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 4767 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + [ + ChangeApplicationState, + ReadApplicationState, + ReadApplicationState, + ChangeApplicationState, + OpenFiles, + RunCommands, + OpenTerminalsOrPlugins, + WriteToStdin, + ], +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap new file mode 100644 index 00000000..57afe877 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 903 +expression: "format!(\"{:#?}\", switch_to_mode_event)" +--- +None diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index d6fcf1fc..5c3c7c0d 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -13,6 +13,8 @@ use std::{ }; use wasmer::{Instance, Module, Store, Value}; use zellij_utils::async_std::task::{self, JoinHandle}; +use zellij_utils::data::{PermissionStatus, PermissionType}; +use zellij_utils::input::permission::PermissionCache; use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; use zellij_utils::plugin_api::event::ProtobufEvent; @@ -706,6 +708,44 @@ impl WasmBridge { }; } } + pub fn cache_plugin_permissions( + &mut self, + plugin_id: PluginId, + client_id: Option, + permissions: Vec, + status: PermissionStatus, + cache_path: Option, + ) -> Result<()> { + if let Some(running_plugin) = self + .plugin_map + .lock() + .unwrap() + .get_running_plugin(plugin_id, client_id) + { + let err_context = || format!("Failed to write plugin permission {plugin_id}"); + + let mut running_plugin = running_plugin.lock().unwrap(); + let permissions = if status == PermissionStatus::Granted { + permissions + } else { + vec![] + }; + + running_plugin + .plugin_env + .set_permissions(HashSet::from_iter(permissions.clone())); + + let mut permission_cache = PermissionCache::from_path_or_default(cache_path); + permission_cache.cache( + running_plugin.plugin_env.plugin.location.to_string(), + permissions, + ); + + permission_cache.write_to_file().with_context(err_context)?; + } + + Ok(()) + } } fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) { @@ -728,6 +768,35 @@ fn handle_plugin_loading_failure( )); } +// TODO: move to permissions? +fn check_event_permission( + plugin_env: &PluginEnv, + event: &Event, +) -> (PermissionStatus, Option) { + if plugin_env.plugin.is_builtin() { + // built-in plugins can do all the things because they're part of the application and + // there's no use to deny them anything + return (PermissionStatus::Granted, None); + } + let permission = match event { + Event::ModeUpdate(..) + | Event::TabUpdate(..) + | Event::PaneUpdate(..) + | Event::CopyToClipboard(..) + | Event::SystemClipboardFailure + | Event::InputReceived => PermissionType::ReadApplicationState, + _ => 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)) +} + pub fn apply_event_to_plugin( plugin_id: PluginId, client_id: ClientId, @@ -739,35 +808,48 @@ pub fn apply_event_to_plugin( plugin_bytes: &mut Vec<(PluginId, ClientId, Vec)>, ) -> Result<()> { let err_context = || format!("Failed to apply event to plugin {plugin_id}"); - let protobuf_event: ProtobufEvent = event - .clone() - .try_into() - .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; - let update = instance - .exports - .get_function("update") - .with_context(err_context)?; - wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec()) - .with_context(err_context)?; - let update_return = update.call(&[]).with_context(err_context)?; - let should_render = match update_return.get(0) { - Some(Value::I32(n)) => *n == 1, - _ => false, - }; - - if rows > 0 && columns > 0 && should_render { - let rendered_bytes = instance - .exports - .get_function("render") - .map_err(anyError::new) - .and_then(|render| { - render - .call(&[Value::I32(rows as i32), Value::I32(columns as i32)]) + match check_event_permission(plugin_env, event) { + (PermissionStatus::Granted, _) => { + let protobuf_event: ProtobufEvent = event + .clone() + .try_into() + .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; + let update = instance + .exports + .get_function("update") + .with_context(err_context)?; + wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec()) + .with_context(err_context)?; + let update_return = update.call(&[]).with_context(err_context)?; + let should_render = match update_return.get(0) { + Some(Value::I32(n)) => *n == 1, + _ => false, + }; + if rows > 0 && columns > 0 && should_render { + let rendered_bytes = instance + .exports + .get_function("render") .map_err(anyError::new) - }) - .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) - .with_context(err_context)?; - plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec())); + .and_then(|render| { + render + .call(&[Value::I32(rows as i32), Value::I32(columns as i32)]) + .map_err(anyError::new) + }) + .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) + .with_context(err_context)?; + plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec())); + } + }, + (PermissionStatus::Denied, permission) => { + log::error!( + "PluginId '{}' permission '{}' is not allowed - Event '{:?}' denied", + plugin_id, + permission + .map(|p| p.to_string()) + .unwrap_or("UNKNOWN".to_owned()), + EventType::from_str(&event.to_string()).with_context(err_context)? + ); + }, } Ok(()) } diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 275de13e..f2effa19 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -8,12 +8,15 @@ use std::{ collections::{BTreeMap, HashSet}, path::PathBuf, process, + str::FromStr, sync::{Arc, Mutex}, thread, time::{Duration, Instant}, }; use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer_wasi::WasiEnv; +use zellij_utils::data::{CommandType, PermissionStatus, PermissionType, PluginPermission}; +use zellij_utils::input::permission::PermissionCache; use url::Url; @@ -86,118 +89,134 @@ impl ForeignFunctionEnv { } fn host_run_plugin_command(env: &ForeignFunctionEnv) { + let err_context = || format!("failed to run plugin command {}", env.plugin_env.name()); wasi_read_bytes(&env.plugin_env.wasi_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 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::GetPluginIds => get_plugin_ids(env), - PluginCommand::GetZellijVersion => get_zellij_version(env), - PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open), - PluginCommand::OpenFileFloating(file_to_open) => { - open_file_floating(env, file_to_open) + match check_command_permission(&env.plugin_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::GetPluginIds => get_plugin_ids(env), + PluginCommand::GetZellijVersion => get_zellij_version(env), + PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open), + PluginCommand::OpenFileFloating(file_to_open) => { + open_file_floating(env, file_to_open) + }, + PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?), + PluginCommand::OpenTerminalFloating(cwd) => { + open_terminal_floating(env, cwd.path.try_into()?) + }, + PluginCommand::OpenCommandPane(command_to_run) => { + open_command_pane(env, command_to_run) + }, + PluginCommand::OpenCommandPaneFloating(command_to_run) => { + open_command_pane_floating(env, command_to_run) + }, + 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::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::NewTab => new_tab(env), + PluginCommand::GoToNextTab => go_to_next_tab(env), + PluginCommand::GoToPreviousTab => go_to_previous_tab(env), + PluginCommand::Resize(resize_payload) => resize(env, resize_payload), + PluginCommand::ResizeWithDirection(resize_strategy) => { + resize_with_direction(env, resize_strategy) + }, + PluginCommand::FocusNextPane => focus_next_pane(env), + PluginCommand::FocusPreviousPane => focus_previous_pane(env), + PluginCommand::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::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) => { + focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden) + }, + PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => { + focus_plugin_pane(env, plugin_pane_id, should_float_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::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?), - PluginCommand::OpenTerminalFloating(cwd) => { - open_terminal_floating(env, cwd.path.try_into()?) + (PermissionStatus::Denied, permission) => { + log::error!( + "Plugin '{}' permission '{}' denied - Command '{:?}' denied", + env.plugin_env.name(), + permission + .map(|p| p.to_string()) + .unwrap_or("UNKNOWN".to_owned()), + CommandType::from_str(&command.to_string()).with_context(err_context)? + ); }, - PluginCommand::OpenCommandPane(command_to_run) => { - open_command_pane(env, command_to_run) - }, - PluginCommand::OpenCommandPaneFloating(command_to_run) => { - open_command_pane_floating(env, command_to_run) - }, - 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::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::NewTab => new_tab(env), - PluginCommand::GoToNextTab => go_to_next_tab(env), - PluginCommand::GoToPreviousTab => go_to_previous_tab(env), - PluginCommand::Resize(resize_payload) => resize(env, resize_payload), - PluginCommand::ResizeWithDirection(resize_strategy) => { - resize_with_direction(env, resize_strategy) - }, - PluginCommand::FocusNextPane => focus_next_pane(env), - PluginCommand::FocusPreviousPane => focus_previous_pane(env), - PluginCommand::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::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) => { - focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden) - }, - PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => { - focus_plugin_pane(env, plugin_pane_id, should_float_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), - } + }; Ok(()) }) .with_context(|| format!("failed to run plugin command {}", env.plugin_env.name())) @@ -255,6 +274,30 @@ fn set_selectable(env: &ForeignFunctionEnv, selectable: bool) { } } +fn request_permission(env: &ForeignFunctionEnv, permissions: Vec) -> Result<()> { + if PermissionCache::from_path_or_default(None) + .check_permissions(env.plugin_env.plugin.location.to_string(), &permissions) + { + return env + .plugin_env + .senders + .send_to_plugin(PluginInstruction::PermissionRequestResult( + env.plugin_env.plugin_id, + Some(env.plugin_env.client_id), + permissions.to_vec(), + PermissionStatus::Granted, + None, + )); + } + + env.plugin_env + .senders + .send_to_screen(ScreenInstruction::RequestPluginPermissions( + env.plugin_env.plugin_id, + PluginPermission::new(env.plugin_env.plugin.location.to_string(), permissions), + )) +} + fn get_plugin_ids(env: &ForeignFunctionEnv) { let ids = PluginIds { plugin_id: env.plugin_env.plugin_id, @@ -1001,3 +1044,81 @@ pub fn wasi_read_bytes(wasi_env: &WasiEnv) -> Result> { .and_then(|string| serde_json::from_str(&string).map_err(anyError::new)) .with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'")) } + +// TODO: move to permissions? +fn check_command_permission( + plugin_env: &PluginEnv, + command: &PluginCommand, +) -> (PermissionStatus, Option) { + if plugin_env.plugin.is_builtin() { + // built-in plugins can do all the things because they're part of the application and + // there's no use to deny them anything + return (PermissionStatus::Granted, None); + } + let permission = match command { + PluginCommand::OpenFile(..) | PluginCommand::OpenFileFloating(..) => { + PermissionType::OpenFiles + }, + PluginCommand::OpenTerminal(..) + | PluginCommand::StartOrReloadPlugin(..) + | PluginCommand::OpenTerminalFloating(..) => PermissionType::OpenTerminalsOrPlugins, + PluginCommand::OpenCommandPane(..) + | PluginCommand::OpenCommandPaneFloating(..) + | PluginCommand::ExecCmd(..) => PermissionType::RunCommands, + PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin, + PluginCommand::SwitchTabTo(..) + | PluginCommand::SwitchToMode(..) + | PluginCommand::NewTabsWithLayout(..) + | PluginCommand::NewTab + | PluginCommand::GoToNextTab + | PluginCommand::GoToPreviousTab + | PluginCommand::Resize(..) + | PluginCommand::ResizeWithDirection(..) + | PluginCommand::FocusNextPane + | PluginCommand::MoveFocus(..) + | PluginCommand::MoveFocusOrTab(..) + | PluginCommand::Detach + | PluginCommand::EditScrollback + | PluginCommand::ToggleTab + | PluginCommand::MovePane + | PluginCommand::MovePaneWithDirection(..) + | PluginCommand::ClearScreen + | PluginCommand::ScrollUp + | PluginCommand::ScrollDown + | PluginCommand::ScrollToTop + | PluginCommand::ScrollToBottom + | PluginCommand::PageScrollUp + | PluginCommand::PageScrollDown + | PluginCommand::ToggleFocusFullscreen + | PluginCommand::TogglePaneFrames + | PluginCommand::TogglePaneEmbedOrEject + | 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::RenameTab(..) => PermissionType::ChangeApplicationState, + _ => return (PermissionStatus::Granted, None), + }; + + log::info!("plugin permissions: {:?}", plugin_env.permissions); + if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() { + if permissions.contains(&permission) { + return (PermissionStatus::Granted, None); + } + } + + (PermissionStatus::Denied, Some(permission)) +} diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 4d5775f7..456bea09 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use std::rc::Rc; use std::str; -use zellij_utils::data::{Direction, PaneManifest, Resize, ResizeStrategy}; +use zellij_utils::data::{Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy}; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::options::Clipboard; @@ -280,6 +280,10 @@ pub enum ScreenInstruction { FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float RenamePane(PaneId, Vec), RenameTab(usize, Vec), + RequestPluginPermissions( + u32, // u32 - plugin_id + PluginPermission, + ), BreakPane(Box, Option, ClientId), BreakPaneRight(ClientId), BreakPaneLeft(ClientId), @@ -450,6 +454,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, + ScreenInstruction::RequestPluginPermissions(..) => { + ScreenContext::RequestPluginPermissions + }, ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane, ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight, ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft, @@ -2958,6 +2965,24 @@ pub(crate) fn screen_thread_main( } screen.report_tab_state()?; }, + ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => { + let all_tabs = screen.get_tabs_mut(); + let found = all_tabs.values_mut().any(|tab| { + if tab.has_plugin(plugin_id) { + tab.request_plugin_permissions(plugin_id, Some(plugin_permission.clone())); + true + } else { + false + } + }); + + if !found { + log::error!( + "PluginId '{}' not found - cannot request permissions", + plugin_id + ); + } + }, ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => { screen.break_pane(default_shell, default_layout, client_id)?; }, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 20fbaaca..fb6d4e17 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -9,7 +9,9 @@ mod swap_layouts; use copy_command::CopyCommand; use std::env::temp_dir; use uuid::Uuid; -use zellij_utils::data::{Direction, PaneInfo, ResizeStrategy}; +use zellij_utils::data::{ + Direction, PaneInfo, PermissionStatus, PermissionType, PluginPermission, ResizeStrategy, +}; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::position::{Column, Line}; @@ -220,6 +222,7 @@ pub trait Pane { fn set_should_render_boundaries(&mut self, _should_render: bool) {} fn selectable(&self) -> bool; fn set_selectable(&mut self, selectable: bool); + fn request_permissions_from_user(&mut self, _permissions: Option) {} fn render( &mut self, client_id: Option, @@ -473,6 +476,7 @@ pub trait Pane { pub enum AdjustedInput { WriteBytesToTerminal(Vec), ReRunCommandInThisPane(RunCommand), + PermissionRequestResult(Vec, PermissionStatus), CloseThisPane, } pub fn get_next_terminal_position( @@ -1560,17 +1564,35 @@ impl Tab { self.close_pane(PaneId::Terminal(active_terminal_id), false, None); should_update_ui = true; }, + Some(_) => {}, None => {}, } }, - PaneId::Plugin(pid) => { - let mut plugin_updates = vec![]; - for key in parse_keys(&input_bytes) { - plugin_updates.push((Some(pid), client_id, Event::Key(key))); - } - self.senders - .send_to_plugin(PluginInstruction::Update(plugin_updates)) - .with_context(err_context)?; + PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal(input_bytes) { + Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => { + let mut plugin_updates = vec![]; + for key in parse_keys(&adjusted_input) { + plugin_updates.push((Some(pid), client_id, Event::Key(key))); + } + self.senders + .send_to_plugin(PluginInstruction::Update(plugin_updates)) + .with_context(err_context)?; + }, + Some(AdjustedInput::PermissionRequestResult(permissions, status)) => { + self.request_plugin_permissions(pid, None); + self.senders + .send_to_plugin(PluginInstruction::PermissionRequestResult( + pid, + client_id, + permissions, + status, + None, + )) + .with_context(err_context)?; + should_update_ui = true; + }, + Some(_) => {}, + None => {}, }, } Ok(should_update_ui) @@ -3437,6 +3459,20 @@ impl Tab { } Ok(()) } + pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option) { + if let Some(plugin_pane) = self + .tiled_panes + .get_pane_mut(PaneId::Plugin(pid)) + .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == PaneId::Plugin(pid)) + }) + { + plugin_pane.request_permissions_from_user(permissions); + } + } } pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box) -> PaneInfo { diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index 67b4a551..df99e9ae 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -107,6 +107,7 @@ macro_rules! register_plugin { fn load() { STATE.with(|state| { use std::collections::BTreeMap; + use std::convert::TryFrom; use std::convert::TryInto; use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration; use zellij_tile::shim::prost::Message; diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index c3a772f9..6204767a 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -39,6 +39,13 @@ pub fn set_selectable(selectable: bool) { unsafe { host_run_plugin_command() }; } +pub fn request_permission(permissions: &[PermissionType]) { + let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + // Query Functions /// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id. pub fn get_plugin_ids() -> PluginIds { diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index f38c500f..9e2112cf 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm index 80a6776e..ff13cefd 100755 Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 58beaacc..e18d4edf 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index db61209a..3aff9cc3 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index 1a2919f6..ee53ba18 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 50600a93..a2c3a42a 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -36,6 +36,8 @@ lazy_static! { .cache_dir() .to_path_buf() .join(format!("{}", Uuid::new_v4())); + pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf = + ZELLIJ_CACHE_DIR.join("permissions.kdl"); } pub const FEATURES: &[&str] = &[ diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 9c1c8e2f..a08e642f 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt; use std::path::{Path, PathBuf}; use std::str::FromStr; -use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; +use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString}; pub type ClientId = u16; // TODO: merge with crate type? @@ -493,6 +493,63 @@ pub enum Event { FileSystemUpdate(Vec), /// A file was deleted somewhere in the Zellij CWD folder FileSystemDelete(Vec), + /// A Result of plugin permission request + PermissionRequestResult(PermissionStatus), +} + +#[derive( + Debug, + PartialEq, + Eq, + Hash, + Copy, + Clone, + EnumDiscriminants, + ToString, + Serialize, + Deserialize, + PartialOrd, + Ord, +)] +#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize, Display, PartialOrd, Ord))] +#[strum_discriminants(name(PermissionType))] +#[non_exhaustive] +pub enum Permission { + ReadApplicationState, + ChangeApplicationState, + OpenFiles, + RunCommands, + OpenTerminalsOrPlugins, + WriteToStdin, +} + +impl PermissionType { + pub fn display_name(&self) -> String { + match self { + PermissionType::ReadApplicationState => { + "Access Zellij state (Panes, Tabs and UI)".to_owned() + }, + PermissionType::ChangeApplicationState => { + "Change Zellij state (Panes, Tabs and UI)".to_owned() + }, + PermissionType::OpenFiles => "Open files (eg. for editing)".to_owned(), + PermissionType::RunCommands => "Run commands".to_owned(), + PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(), + PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(), + } + } +} + +#[derive(Debug, Clone)] +pub struct PluginPermission { + pub name: String, + pub permissions: Vec, +} + +impl PluginPermission { + pub fn new(name: String, permissions: Vec) -> Self { + PluginPermission { name, permissions } + } } /// Describes the different input modes, which change the way that keystrokes will be interpreted. @@ -811,6 +868,12 @@ pub enum CopyDestination { System, } +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +pub enum PermissionStatus { + Granted, + Denied, +} + #[derive(Debug, Default, Clone)] pub struct FileToOpen { pub path: PathBuf, @@ -882,7 +945,9 @@ impl PluginMessage { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, EnumDiscriminants, ToString)] +#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] +#[strum_discriminants(name(CommandType))] pub enum PluginCommand { Subscribe(HashSet), Unsubscribe(HashSet), @@ -950,4 +1015,5 @@ pub enum PluginCommand { RenamePluginPane(u32, String), // plugin pane id, new name RenameTab(u32, String), // tab index, new name ReportPanic(String), // stringified panic + RequestPluginPermissions(Vec), } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index df5d0278..306db448 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -338,6 +338,7 @@ pub enum ScreenContext { FocusPaneWithId, RenamePane, RenameTab, + RequestPluginPermissions, BreakPane, BreakPaneRight, BreakPaneLeft, @@ -377,6 +378,7 @@ pub enum PluginContext { PostMessageToPluginWorker, PostMessageToPlugin, PluginSubscribedToEvents, + PermissionRequestResult, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 6722cfd8..1c95f348 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -4,6 +4,7 @@ pub mod config; pub mod keybinds; pub mod layout; pub mod options; +pub mod permission; pub mod plugins; pub mod theme; diff --git a/zellij-utils/src/input/permission.rs b/zellij-utils/src/input/permission.rs new file mode 100644 index 00000000..1755fe1e --- /dev/null +++ b/zellij-utils/src/input/permission.rs @@ -0,0 +1,60 @@ +use std::{ + collections::HashMap, + fs::{self, File}, + io::Write, + path::PathBuf, +}; + +use crate::{consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType}; + +pub type GrantedPermission = HashMap>; + +#[derive(Default, Debug)] +pub struct PermissionCache { + path: PathBuf, + granted: GrantedPermission, +} + +impl PermissionCache { + pub fn cache(&mut self, plugin_name: String, permissions: Vec) { + self.granted.insert(plugin_name, permissions); + } + + pub fn get_permissions(&self, plugin_name: String) -> Option<&Vec> { + self.granted.get(&plugin_name) + } + + pub fn check_permissions( + &self, + plugin_name: String, + permissions: &Vec, + ) -> bool { + if let Some(target) = self.granted.get(&plugin_name) { + if target == permissions { + return true; + } + } + + false + } + + pub fn from_path_or_default(cache_path: Option) -> Self { + let cache_path = cache_path.unwrap_or(ZELLIJ_PLUGIN_PERMISSIONS_CACHE.to_path_buf()); + + let granted = match fs::read_to_string(cache_path.clone()) { + Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(), + Err(_) => GrantedPermission::default(), + }; + + PermissionCache { + path: cache_path, + granted, + } + } + + pub fn write_to_file(&self) -> std::io::Result<()> { + let mut f = File::create(&self.path)?; + write!(f, "{}", PermissionCache::to_string(&self.granted))?; + Ok(()) + } +} diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index a75a4a63..13a03ad1 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,15 +1,16 @@ mod kdl_layout_parser; -use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize}; +use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, PermissionType, Resize}; use crate::envs::EnvironmentVariables; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::keybinds::Keybinds; use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; use crate::input::options::{Clipboard, OnForceClose, Options}; +use crate::input::permission::{GrantedPermission, PermissionCache}; use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig}; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; use crate::setup::{find_default_config_dir, get_layout_dir}; use kdl_layout_parser::KdlLayoutParser; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use strum::IntoEnumIterator; use miette::NamedSource; @@ -1793,6 +1794,53 @@ impl Themes { } } +impl PermissionCache { + pub fn from_string(raw_string: String) -> Result { + let kdl_document: KdlDocument = raw_string.parse()?; + + let mut granted_permission = GrantedPermission::default(); + + for node in kdl_document.nodes() { + if let Some(children) = node.children() { + let key = kdl_name!(node); + let permissions: Vec = children + .nodes() + .iter() + .filter_map(|p| { + let v = kdl_name!(p); + PermissionType::from_str(v).ok() + }) + .collect(); + + granted_permission.insert(key.into(), permissions); + } + } + + Ok(granted_permission) + } + + pub fn to_string(granted: &GrantedPermission) -> String { + let mut kdl_doucment = KdlDocument::new(); + + granted.iter().for_each(|(k, v)| { + let mut node = KdlNode::new(k.as_str()); + let mut children = KdlDocument::new(); + + let permissions: HashSet = v.clone().into_iter().collect(); + permissions.iter().for_each(|f| { + let n = KdlNode::new(f.to_string().as_str()); + children.nodes_mut().push(n); + }); + + node.set_children(children); + kdl_doucment.nodes_mut().push(node); + }); + + kdl_doucment.fmt(); + kdl_doucment.to_string() + } +} + pub fn parse_plugin_user_configuration( plugin_block: &KdlNode, ) -> Result, ConfigError> { diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 95928ae3..3a9838fa 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -38,6 +38,7 @@ enum EventType { FileSystemUpdate = 13; /// A file was deleted somewhere in the Zellij CWD folder FileSystemDelete = 14; + PermissionRequestResult = 15; } message EventNameList { @@ -57,9 +58,14 @@ message Event { bool visible_payload = 9; CustomMessagePayload custom_message_payload = 10; FileListPayload file_list_payload = 11; + PermissionRequestResultPayload permission_request_result_payload = 12; } } +message PermissionRequestResultPayload { + bool granted = 1; +} + message FileListPayload { repeated string paths = 1; } diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 315fa54b..6115fe1e 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -13,9 +13,10 @@ pub use super::generated_api::api::{ style::Style as ProtobufStyle, }; use crate::data::{ - CopyDestination, Direction, Event, EventType, InputMode, Key, ModeInfo, Mouse, Palette, - PaletteColor, PaneInfo, PaneManifest, PluginCapabilities, Style, TabInfo, ThemeHue, + CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest, + PermissionStatus, PluginCapabilities, Style, TabInfo, }; + use crate::errors::prelude::*; use crate::input::actions::Action; @@ -160,6 +161,16 @@ impl TryFrom for Event { }, _ => Err("Malformed payload for the file system delete Event"), }, + Some(ProtobufEventType::PermissionRequestResult) => match protobuf_event.payload { + Some(ProtobufEventPayload::PermissionRequestResultPayload(payload)) => { + if payload.granted { + Ok(Event::PermissionRequestResult(PermissionStatus::Granted)) + } else { + Ok(Event::PermissionRequestResult(PermissionStatus::Denied)) + } + }, + _ => Err("Malformed payload for the file system delete Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -290,6 +301,18 @@ impl TryFrom for ProtobufEvent { payload: Some(event::Payload::FileListPayload(file_list_payload)), }) }, + Event::PermissionRequestResult(permission_status) => { + let granted = match permission_status { + PermissionStatus::Granted => true, + PermissionStatus::Denied => false, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::PermissionRequestResult as i32, + payload: Some(event::Payload::PermissionRequestResultPayload( + PermissionRequestResultPayload { granted }, + )), + }) + }, } } } @@ -673,6 +696,7 @@ impl TryFrom for EventType { ProtobufEventType::FileSystemRead => EventType::FileSystemRead, ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate, ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, + ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult, }) } } @@ -696,6 +720,7 @@ impl TryFrom for ProtobufEventType { EventType::FileSystemRead => ProtobufEventType::FileSystemRead, EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate, EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, + EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult, }) } } @@ -717,6 +742,7 @@ fn serialize_mode_update_event() { #[test] fn serialize_mode_update_event_with_non_default_values() { + use crate::data::{Direction, Palette, PaletteColor, ThemeHue}; use prost::Message; let mode_update_event = Event::ModeUpdate(ModeInfo { mode: InputMode::Locked, diff --git a/zellij-utils/src/plugin_api/mod.rs b/zellij-utils/src/plugin_api/mod.rs index 55ace500..4812d468 100644 --- a/zellij-utils/src/plugin_api/mod.rs +++ b/zellij-utils/src/plugin_api/mod.rs @@ -7,6 +7,7 @@ pub mod key; pub mod message; pub mod plugin_command; pub mod plugin_ids; +pub mod plugin_permission; pub mod resize; pub mod style; pub mod generated_api { diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index f09ccf4b..9ba8bb7f 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -6,6 +6,7 @@ import "command.proto"; import "message.proto"; import "input_mode.proto"; import "resize.proto"; +import "plugin_permission.proto"; package api.plugin_command; @@ -76,6 +77,7 @@ enum CommandName { RenamePluginPane = 63; RenameTab = 64; ReportCrash = 65; + RequestPluginPermissions = 66; } message PluginCommand { @@ -117,9 +119,14 @@ message PluginCommand { IdAndNewName rename_plugin_pane_payload = 35; IdAndNewName rename_tab_payload = 36; string report_crash_payload = 37; + RequestPluginPermissionPayload request_plugin_permission_payload = 38; } } +message RequestPluginPermissionPayload { + repeated plugin_permission.PermissionType permissions = 1; +} + message SubscribePayload { event.EventNameList subscriptions = 1; } diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index bf359dd2..ff50a6a7 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -3,14 +3,15 @@ pub use super::generated_api::api::{ plugin_command::{ plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload, OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat, - PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ResizePayload, - SetTimeoutPayload, SubscribePayload, SwitchTabToPayload, SwitchToModePayload, - UnsubscribePayload, + PluginCommand as ProtobufPluginCommand, PluginMessagePayload, + RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload, + SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload, }, + plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, }; -use crate::data::PluginCommand; +use crate::data::{PermissionType, PluginCommand}; use std::convert::TryFrom; @@ -486,6 +487,19 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for ReportCrash"), }, + Some(CommandName::RequestPluginPermissions) => match protobuf_plugin_command.payload { + Some(Payload::RequestPluginPermissionPayload(payload)) => { + Ok(PluginCommand::RequestPluginPermissions( + payload + .permissions + .iter() + .filter_map(|p| ProtobufPermissionType::from_i32(*p)) + .filter_map(|p| PermissionType::try_from(p).ok()) + .collect(), + )) + }, + _ => Err("Mismatched payload for RequestPluginPermission"), + }, None => Err("Unrecognized plugin command"), } } @@ -820,6 +834,18 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::ReportCrash as i32, payload: Some(Payload::ReportCrashPayload(payload)), }), + PluginCommand::RequestPluginPermissions(permissions) => Ok(ProtobufPluginCommand { + name: CommandName::RequestPluginPermissions as i32, + payload: Some(Payload::RequestPluginPermissionPayload( + RequestPluginPermissionPayload { + permissions: permissions + .iter() + .filter_map(|p| ProtobufPermissionType::try_from(*p).ok()) + .map(|p| p as i32) + .collect(), + }, + )), + }), } } } diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto new file mode 100644 index 00000000..50072b1b --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_permission.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package api.plugin_permission; + +enum PermissionType { + ReadApplicationState = 0; + ChangeApplicationState = 1; + OpenFiles = 2; + RunCommands = 3; + OpenTerminalsOrPlugins = 4; + WriteToStdin = 5; +} diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs new file mode 100644 index 00000000..d2839fac --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_permission.rs @@ -0,0 +1,44 @@ +pub use super::generated_api::api::plugin_permission::PermissionType as ProtobufPermissionType; +use crate::data::PermissionType; + +use std::convert::TryFrom; + +impl TryFrom for PermissionType { + type Error = &'static str; + fn try_from(protobuf_permission: ProtobufPermissionType) -> Result { + match protobuf_permission { + ProtobufPermissionType::ReadApplicationState => { + Ok(PermissionType::ReadApplicationState) + }, + ProtobufPermissionType::ChangeApplicationState => { + Ok(PermissionType::ChangeApplicationState) + }, + ProtobufPermissionType::OpenFiles => Ok(PermissionType::OpenFiles), + ProtobufPermissionType::RunCommands => Ok(PermissionType::RunCommands), + ProtobufPermissionType::OpenTerminalsOrPlugins => { + Ok(PermissionType::OpenTerminalsOrPlugins) + }, + ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin), + } + } +} + +impl TryFrom for ProtobufPermissionType { + type Error = &'static str; + fn try_from(permission: PermissionType) -> Result { + match permission { + PermissionType::ReadApplicationState => { + Ok(ProtobufPermissionType::ReadApplicationState) + }, + PermissionType::ChangeApplicationState => { + Ok(ProtobufPermissionType::ChangeApplicationState) + }, + PermissionType::OpenFiles => Ok(ProtobufPermissionType::OpenFiles), + PermissionType::RunCommands => Ok(ProtobufPermissionType::RunCommands), + PermissionType::OpenTerminalsOrPlugins => { + Ok(ProtobufPermissionType::OpenTerminalsOrPlugins) + }, + PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin), + } + } +}