diff options
author | Aram Drevekenin <aram@poor.dev> | 2024-02-06 14:26:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-06 14:26:14 +0100 |
commit | 6b20a958f4e8db5614fffb27fe5d32f07ddfe855 (patch) | |
tree | c8383fb7941b5e0c23cb348a655f2ea0fafd8668 /zellij-server | |
parent | 286f7ccc28e3a091e2d3ef5663ae7878056551e6 (diff) |
feat(sessions): welcome screen (#3112)
* prototype - can send layout name for new session from session-manager
* feat(sessions): ui for selecting layout for new session in the session-manager
* fix: send available layouts to plugins
* make tests compile
* fix tests
* improve ui
* fix: respect built-in layouts
* ui for built-in layouts
* some cleanups
* style(fmt): rustfmt
* welcome screen ui
* fix: make sure layout config is not shared between sessions
* allow disconnecting other users from current session and killing other sessions
* fix: respect default layout
* add welcome screen layout
* tests(plugins): new api methods
* fix(session-manager): do not quit welcome screen on esc and break
* fix(plugins): adjust permissions
* style(fmt): rustfmt
* style(fmt): fix warnings
Diffstat (limited to 'zellij-server')
9 files changed, 392 insertions, 9 deletions
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 437f465bd..008955705 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -44,7 +44,7 @@ use zellij_utils::{ consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, data::{ConnectToSession, Event, PluginCapabilities}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, - home::get_default_data_dir, + home::{default_layout_dir, get_default_data_dir}, input::{ command::{RunCommand, TerminalAction}, get_mode_info, @@ -93,6 +93,7 @@ pub enum ServerInstruction { pipe_id: String, client_id: ClientId, }, + DisconnectAllClientsExcept(ClientId), } impl From<&ServerInstruction> for ServerContext { @@ -117,6 +118,9 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::AssociatePipeWithClient { .. } => { ServerContext::AssociatePipeWithClient }, + ServerInstruction::DisconnectAllClientsExcept(..) => { + ServerContext::DisconnectAllClientsExcept + }, } } } @@ -133,6 +137,7 @@ pub(crate) struct SessionMetaData { pub client_attributes: ClientAttributes, pub default_shell: Option<TerminalAction>, pub layout: Box<Layout>, + pub config_options: Box<Options>, screen_thread: Option<thread::JoinHandle<()>>, pty_thread: Option<thread::JoinHandle<()>>, plugin_thread: Option<thread::JoinHandle<()>>, @@ -650,6 +655,21 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { } break; }, + ServerInstruction::DisconnectAllClientsExcept(client_id) => { + let client_ids: Vec<ClientId> = session_state + .read() + .unwrap() + .client_ids() + .iter() + .copied() + .filter(|c| c != &client_id) + .collect(); + for client_id in client_ids { + let _ = os_input + .send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + } + }, ServerInstruction::DetachSession(client_ids) => { for client_id in client_ids { let _ = os_input @@ -749,7 +769,16 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { session_state ); }, - ServerInstruction::SwitchSession(connect_to_session, client_id) => { + ServerInstruction::SwitchSession(mut connect_to_session, client_id) => { + let layout_dir = session_data + .read() + .unwrap() + .as_ref() + .and_then(|c| c.config_options.layout_dir.clone()) + .or_else(|| default_layout_dir()); + if let Some(layout_dir) = layout_dir { + connect_to_session.apply_layout_dir(&layout_dir); + } if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { session_data .write() @@ -906,6 +935,7 @@ fn init_session( let client_attributes_clone = client_attributes.clone(); let debug = opts.debug; let layout = layout.clone(); + let config_options = config_options.clone(); move || { screen_thread_main( screen_bus, @@ -1006,6 +1036,7 @@ fn init_session( default_shell, client_attributes, layout, + config_options: config_options.clone(), screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), plugin_thread: Some(plugin_thread), diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 6791213c7..6b6e345e1 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -5970,3 +5970,243 @@ pub fn pipe_message_to_plugin_plugin_command() { }); assert_snapshot!(format!("{:#?}", plugin_bytes_event)); } + +#[test] +#[ignore] +pub fn switch_session_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, server_receiver, screen_receiver, 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()); + 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 = 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 received_server_instruction = Arc::new(Mutex::new(vec![])); + let server_thread = log_actions_in_thread!( + received_server_instruction, + ServerInstruction::SwitchSession, + server_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + None, + false, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('5')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(500)); + teardown(); + server_thread.join().unwrap(); // this might take a while if the cache is cold + let switch_session_event = received_server_instruction + .lock() + .unwrap() + .iter() + .rev() + .find_map(|i| { + if let ServerInstruction::SwitchSession(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", switch_session_event)); +} + +#[test] +#[ignore] +pub fn switch_session_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, server_receiver, screen_receiver, 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()); + 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 = 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 received_server_instruction = Arc::new(Mutex::new(vec![])); + let server_thread = log_actions_in_thread!( + received_server_instruction, + ServerInstruction::SwitchSession, + server_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + None, + false, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('7')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(500)); + teardown(); + server_thread.join().unwrap(); // this might take a while if the cache is cold + let switch_session_event = received_server_instruction + .lock() + .unwrap() + .iter() + .rev() + .find_map(|i| { + if let ServerInstruction::SwitchSession(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", switch_session_event)); +} + +#[test] +#[ignore] +pub fn disconnect_other_clients_plugins_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, server_receiver, screen_receiver, 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()); + 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 = 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 received_server_instruction = Arc::new(Mutex::new(vec![])); + let server_thread = log_actions_in_thread!( + received_server_instruction, + ServerInstruction::DisconnectAllClientsExcept, + server_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + None, + false, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('6')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(500)); + teardown(); + server_thread.join().unwrap(); // this might take a while if the cache is cold + let switch_session_event = received_server_instruction + .lock() + .unwrap() + .iter() + .rev() + .find_map(|i| { + if let ServerInstruction::DisconnectAllClientsExcept(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", switch_session_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap new file mode 100644 index 000000000..d463df1c0 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap @@ -0,0 +1,10 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 6131 +expression: "format!(\"{:#?}\", switch_session_event)" +--- +Some( + DisconnectAllClientsExcept( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap new file mode 100644 index 000000000..f254f0398 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap @@ -0,0 +1,18 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 6051 +expression: "format!(\"{:#?}\", switch_session_event)" +--- +Some( + SwitchSession( + ConnectToSession { + name: Some( + "my_new_session", + ), + tab_position: None, + pane_id: None, + layout: None, + }, + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap new file mode 100644 index 000000000..46f6819a5 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap @@ -0,0 +1,22 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 6131 +expression: "format!(\"{:#?}\", switch_session_event)" +--- +Some( + SwitchSession( + ConnectToSession { + name: Some( + "my_other_new_session", + ), + tab_position: None, + pane_id: None, + layout: Some( + BuiltIn( + "compact", + ), + ), + }, + 1, + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 8791e128e..e6787c240 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -18,10 +18,14 @@ use std::{ use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports}; use wasmer_wasi::WasiEnv; use zellij_utils::data::{ - CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType, - PluginPermission, + CommandType, ConnectToSession, HttpVerb, LayoutInfo, MessageToPlugin, PermissionStatus, + PermissionType, PluginPermission, }; use zellij_utils::input::permission::PermissionCache; +use zellij_utils::{ + interprocess::local_socket::LocalSocketStream, + ipc::{ClientToServerMsg, IpcSenderWithContext}, +}; use url::Url; @@ -225,6 +229,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) { connect_to_session.name, connect_to_session.tab_position, connect_to_session.pane_id, + connect_to_session.layout, )?, PluginCommand::DeleteDeadSession(session_name) => { delete_dead_session(session_name)? @@ -252,6 +257,8 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) { cli_pipe_output(env, pipe_name, output)? }, PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?, + PluginCommand::DisconnectOtherClients => disconnect_other_clients(env), + PluginCommand::KillSessions(session_list) => kill_sessions(session_list), }, (PermissionStatus::Denied, permission) => { log::error!( @@ -900,6 +907,7 @@ fn switch_session( session_name: Option<String>, tab_position: Option<usize>, pane_id: Option<(u32, bool)>, + layout: Option<LayoutInfo>, ) -> Result<()> { // pane_id is (id, is_plugin) let err_context = || format!("Failed to switch session"); @@ -909,6 +917,7 @@ fn switch_session( name: session_name, tab_position, pane_id, + layout, }; env.plugin_env .senders @@ -1278,6 +1287,30 @@ fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) { apply_action!(action, error_msg, env); } +fn disconnect_other_clients(env: &ForeignFunctionEnv) { + let _ = env + .plugin_env + .senders + .send_to_server(ServerInstruction::DisconnectAllClientsExcept( + env.plugin_env.client_id, + )) + .context("failed to send disconnect other clients instruction"); +} + +fn kill_sessions(session_names: Vec<String>) { + for session_name in session_names { + let path = &*ZELLIJ_SOCK_DIR.join(&session_name); + match LocalSocketStream::connect(path) { + Ok(stream) => { + let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession); + }, + Err(e) => { + log::error!("Failed to kill session {}: {:?}", session_name, e); + }, + }; + } +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -1406,7 +1439,9 @@ fn check_command_permission( | PluginCommand::DeleteDeadSession(..) | PluginCommand::DeleteAllDeadSessions | PluginCommand::RenameSession(..) - | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState, + | PluginCommand::RenameTab(..) + | PluginCommand::DisconnectOtherClients + | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c312480dc..5b0b6bea1 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -577,6 +577,8 @@ pub(crate) struct Screen { default_shell: Option<PathBuf>, styled_underlines: bool, arrow_fonts: bool, + layout_dir: Option<PathBuf>, + default_layout_name: Option<String>, } impl Screen { @@ -592,12 +594,14 @@ impl Screen { copy_options: CopyOptions, debug: bool, default_layout: Box<Layout>, + default_layout_name: Option<String>, default_shell: Option<PathBuf>, session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option<usize>, styled_underlines: bool, arrow_fonts: bool, + layout_dir: Option<PathBuf>, ) -> Self { let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_info = SessionInfo::new(session_name.clone()); @@ -629,6 +633,7 @@ impl Screen { session_name, session_infos_on_machine, default_layout, + default_layout_name, default_shell, session_serialization, serialize_pane_viewport, @@ -636,6 +641,7 @@ impl Screen { styled_underlines, arrow_fonts, resurrectable_sessions, + layout_dir, } } @@ -1412,12 +1418,21 @@ impl Screen { // generate own session info let pane_manifest = self.generate_and_report_pane_state()?; let tab_infos = self.generate_and_report_tab_state()?; + // in the context of unit/integration tests, we don't need to list available layouts + // because this is mostly about HD access - it does however throw off the timing in the + // tests and causes them to flake, which is why we skip it here + #[cfg(not(test))] + let available_layouts = + Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name); + #[cfg(test)] + let available_layouts = vec![]; let session_info = SessionInfo { name: self.session_name.clone(), tabs: tab_infos, panes: pane_manifest, connected_clients: self.active_tab_indices.keys().len(), is_current_session: true, + available_layouts, }; self.bus .senders @@ -2101,7 +2116,11 @@ pub(crate) fn screen_thread_main( let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false); let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize; let session_is_mirrored = config_options.mirror_session.unwrap_or(false); + let layout_dir = config_options.layout_dir; let default_shell = config_options.default_shell; + let default_layout_name = config_options + .default_layout + .map(|l| format!("{}", l.display())); let copy_options = CopyOptions::new( config_options.copy_command, config_options.copy_clipboard.unwrap_or_default(), @@ -2128,12 +2147,14 @@ pub(crate) fn screen_thread_main( copy_options, debug, default_layout, + default_layout_name, default_shell, session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, styled_underlines, arrow_fonts, + layout_dir, ); let mut pending_tab_ids: HashSet<usize> = HashSet::new(); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 0352eb96d..2ed9eb35b 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -847,7 +847,7 @@ impl Tab { pub fn rename_session(&mut self, new_session_name: String) -> Result<()> { { let mode_infos = &mut self.mode_info.borrow_mut(); - for (_client_id, mut mode_info) in mode_infos.iter_mut() { + for (_client_id, mode_info) in mode_infos.iter_mut() { mode_info.session_name = Some(new_session_name.clone()); } self.default_mode_info.session_name = Some(new_session_name); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 34076d700..4ce4dbc14 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -241,10 +241,12 @@ fn create_new_screen(size: Size) -> Screen { let session_is_mirrored = true; let copy_options = CopyOptions::default(); let default_layout = Box::new(Layout::default()); + le |