diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap index 7dbfbbd1..bfb3fcd9 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap @@ -26,4 +26,4 @@ expression: first_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  - <←↓↑→> Move focus / New / Close / Rename / Sync / Toggle / Select pane + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 391a03f6..6a94742c 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -318,6 +318,14 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { } else { spawn_tabs(None); } + session_data + .read() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::AddClient(client_id)) + .unwrap(); } ServerInstruction::AttachClient(attrs, options, client_id) => { let rlock = session_data.read().unwrap(); @@ -339,6 +347,10 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_screen(ScreenInstruction::AddClient(client_id)) .unwrap(); + session_data + .senders + .send_to_plugin(PluginInstruction::AddClient(client_id)) + .unwrap(); let default_mode = options.default_mode.unwrap_or_default(); let mode_info = get_mode_info(default_mode, attrs.palette, session_data.capabilities); @@ -351,12 +363,11 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_plugin(PluginInstruction::Update( None, + Some(client_id), Event::ModeUpdate(mode_info), )) .unwrap(); - for client_id in session_state.read().unwrap().clients.keys() { - os_input.send_to_client(*client_id, ServerToClientMsg::SwitchToMode(mode)); - } + os_input.send_to_client(client_id, ServerToClientMsg::SwitchToMode(mode)); } ServerInstruction::UnblockInputThread => { for client_id in session_state.read().unwrap().clients.keys() { @@ -385,6 +396,14 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_screen(ScreenInstruction::RemoveClient(client_id)) .unwrap(); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::RemoveClient(client_id)) + .unwrap(); } if session_state.read().unwrap().clients.is_empty() { *session_data.write().unwrap() = None; @@ -412,6 +431,14 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_screen(ScreenInstruction::RemoveClient(client_id)) .unwrap(); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::RemoveClient(client_id)) + .unwrap(); } } ServerInstruction::KillSession => { @@ -444,6 +471,14 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_screen(ScreenInstruction::RemoveClient(client_id)) .unwrap(); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::RemoveClient(client_id)) + .unwrap(); } } ServerInstruction::Render(mut output) => { diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index e34145f4..a9d5d3e5 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -131,7 +131,10 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } - fn render(&mut self) -> Option { + fn render(&mut self, client_id: Option) -> Option { + // this is a bit of a hack but works in a pinch + client_id?; + let client_id = client_id.unwrap(); // if self.should_render { if true { // while checking should_render rather than rendering each pane every time @@ -145,6 +148,7 @@ impl Pane for PluginPane { .send(PluginInstruction::Render( buf_tx, self.pid, + client_id, self.get_content_rows(), self.get_content_columns(), )) @@ -274,6 +278,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::ScrollUp(count)), )) .unwrap(); @@ -282,6 +287,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::ScrollDown(count)), )) .unwrap(); @@ -293,6 +299,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::LeftClick(start.line.0, start.column.0)), )) .unwrap(); @@ -301,6 +308,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::Hold(position.line.0, position.column.0)), )) .unwrap(); @@ -309,6 +317,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::Release( end.map(|Position { line, column }| (line.0, column.0)), )), @@ -342,6 +351,7 @@ impl Pane for PluginPane { self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), + None, Event::Mouse(Mouse::RightClick(to.line.0, to.column.0)), )) .unwrap(); diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index f2e34032..a6da7670 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -183,7 +183,8 @@ impl Pane for TerminalPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } - fn render(&mut self) -> Option { + fn render(&mut self, _client_id: Option) -> Option { + // we don't use client_id because terminal panes render the same for all users if self.should_render() { let mut vte_output = String::new(); let mut character_styles = CharacterStyles::new(); diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 8bb82c12..e24d2d4f 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -31,7 +31,11 @@ fn route_action( let mut should_break = false; session .senders - .send_to_plugin(PluginInstruction::Update(None, Event::InputReceived)) + .send_to_plugin(PluginInstruction::Update( + None, + Some(client_id), + Event::InputReceived, + )) .unwrap(); match action { Action::ToggleTab => { @@ -70,6 +74,7 @@ fn route_action( .senders .send_to_plugin(PluginInstruction::Update( None, + Some(client_id), Event::ModeUpdate(get_mode_info(mode, palette, session.capabilities)), )) .unwrap(); @@ -379,12 +384,8 @@ pub(crate) fn route_thread_main( ClientToServerMsg::Action(action) => { if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { if let Action::SwitchToMode(input_mode) = action { - for client_id in session_state.read().unwrap().clients.keys() { - os_input.send_to_client( - *client_id, - ServerToClientMsg::SwitchToMode(input_mode), - ); - } + os_input + .send_to_client(client_id, ServerToClientMsg::SwitchToMode(input_mode)); } if route_action(action, rlocked_sessions, &*os_input, &to_server, client_id) { break; diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 48b57792..1400235e 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -183,7 +183,8 @@ pub(crate) struct Screen { /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap, tab_history: BTreeMap>, - mode_info: ModeInfo, + mode_info: BTreeMap, + default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication colors: Palette, draw_pane_frames: bool, } @@ -206,7 +207,8 @@ impl Screen { tabs: BTreeMap::new(), overlay: OverlayWindow::default(), tab_history: BTreeMap::new(), - mode_info, + mode_info: BTreeMap::new(), + default_mode_info: mode_info, draw_pane_frames, } } @@ -241,12 +243,15 @@ impl Screen { } } fn move_clients(&mut self, source_index: usize, destination_index: usize) { - let connected_clients_in_source_tab = { + let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = { let source_tab = self.tabs.get_mut(&source_index).unwrap(); source_tab.drain_connected_clients() }; let destination_tab = self.tabs.get_mut(&destination_index).unwrap(); - destination_tab.add_multiple_clients(&connected_clients_in_source_tab); + destination_tab.add_multiple_clients( + connected_clients_in_source_tab, + client_mode_infos_in_source_tab, + ); } /// A helper function to switch to a new tab at specified position. fn switch_active_tab(&mut self, new_tab_pos: usize, client_id: ClientId) { @@ -423,6 +428,11 @@ impl Screen { pub fn new_tab(&mut self, layout: Layout, new_pids: Vec, client_id: ClientId) { let tab_index = self.get_new_tab_index(); let position = self.tabs.len(); + let client_mode_info = self + .mode_info + .get(&client_id) + .unwrap_or(&self.default_mode_info) + .clone(); let mut tab = Tab::new( tab_index, position, @@ -431,16 +441,20 @@ impl Screen { self.bus.os_input.as_ref().unwrap().clone(), self.bus.senders.clone(), self.max_panes, - self.mode_info.clone(), + client_mode_info, self.colors, self.draw_pane_frames, client_id, ); - tab.apply_layout(layout, new_pids, tab_index); + tab.apply_layout(layout, new_pids, tab_index, client_id); if let Some(active_tab) = self.get_active_tab_mut(client_id) { active_tab.visible(false); - let connected_clients = active_tab.drain_connected_clients(); - tab.add_multiple_clients(&connected_clients); + let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = + active_tab.drain_connected_clients(); + tab.add_multiple_clients( + connected_clients_in_source_tab, + client_mode_infos_in_source_tab, + ); } for (client_id, tab_history) in &mut self.tab_history { let old_active_index = self.active_tab_indices.remove(client_id).unwrap(); @@ -448,6 +462,7 @@ impl Screen { tab_history.retain(|&e| e != tab_index); tab_history.push(old_active_index); } + tab.update_input_modes(); tab.visible(true); self.tabs.insert(tab_index, tab); if !self.active_tab_indices.contains_key(&client_id) { @@ -503,7 +518,11 @@ impl Screen { } self.bus .senders - .send_to_plugin(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) + .send_to_plugin(PluginInstruction::Update( + None, + None, + Event::TabUpdate(tab_data), + )) .unwrap(); } } @@ -526,7 +545,12 @@ impl Screen { self.update_tabs(); } pub fn change_mode(&mut self, mode_info: ModeInfo, client_id: ClientId) { - if self.mode_info.mode == InputMode::Scroll + let previous_mode = self + .mode_info + .get(&client_id) + .unwrap_or(&self.default_mode_info) + .mode; + if previous_mode == InputMode::Scroll && (mode_info.mode == InputMode::Normal || mode_info.mode == InputMode::Locked) { self.get_active_tab_mut(client_id) @@ -534,9 +558,9 @@ impl Screen { .clear_active_terminal_scroll(client_id); } self.colors = mode_info.palette; - self.mode_info = mode_info; + self.mode_info.insert(client_id, mode_info.clone()); for tab in self.tabs.values_mut() { - tab.mode_info = self.mode_info.clone(); + tab.change_mode_info(mode_info.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); } } @@ -1100,6 +1124,7 @@ pub(crate) fn screen_thread_main( } ScreenInstruction::AddClient(client_id) => { screen.add_client(client_id); + screen.update_tabs(); screen.render(); } diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index dec0c097..f684b996 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -137,7 +137,8 @@ pub(crate) struct Tab { pub senders: ThreadSenders, synchronize_is_active: bool, should_clear_display_before_rendering: bool, - pub mode_info: ModeInfo, + mode_info: HashMap, + default_mode_info: ModeInfo, pub colors: Palette, connected_clients: HashSet, draw_pane_frames: bool, @@ -179,7 +180,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 render(&mut self) -> Option; + fn render(&mut self, client_id: Option) -> Option; fn render_frame(&mut self, client_id: ClientId, frame_params: FrameParams) -> Option; fn render_fake_cursor( &mut self, @@ -342,7 +343,8 @@ impl Tab { os_api, senders, should_clear_display_before_rendering: false, - mode_info, + mode_info: HashMap::new(), + default_mode_info: mode_info, colors, draw_pane_frames, // at the moment this is hard-coded while the feature is being developed @@ -353,7 +355,13 @@ impl Tab { } } - pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec, tab_index: usize) { + pub fn apply_layout( + &mut self, + layout: Layout, + new_pids: Vec, + tab_index: usize, + client_id: ClientId, + ) { // TODO: this should be an attribute on Screen instead of full_screen_ws let free_space = PaneGeom::default(); self.panes_to_hide.clear(); @@ -384,7 +392,7 @@ impl Tab { let (pid_tx, pid_rx) = channel(); let pane_title = run.location.to_string(); self.senders - .send_to_plugin(PluginInstruction::Load(pid_tx, run, tab_index)) + .send_to_plugin(PluginInstruction::Load(pid_tx, run, tab_index, client_id)) .unwrap(); let pid = pid_rx.recv().unwrap(); let mut new_plugin = PluginPane::new( @@ -395,13 +403,6 @@ impl Tab { ); new_plugin.set_borderless(layout.borderless); self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); - // Send an initial mode update to the newly loaded plugin only! - self.senders - .send_to_plugin(PluginInstruction::Update( - Some(pid), - Event::ModeUpdate(self.mode_info.clone()), - )) - .unwrap(); } else { // there are still panes left to fill, use the pids we received in this method let pid = new_pids.next().unwrap(); // if this crashes it means we got less pids than there are panes in this layout @@ -465,12 +466,30 @@ impl Tab { } } } + pub fn update_input_modes(&mut self) { + // this updates all plugins with the client's input mode + for client_id in &self.connected_clients { + let mode_info = self + .mode_info + .get(client_id) + .unwrap_or(&self.default_mode_info); + self.senders + .send_to_plugin(PluginInstruction::Update( + None, + Some(*client_id), + Event::ModeUpdate(mode_info.clone()), + )) + .unwrap(); + } + } pub fn add_client(&mut self, client_id: ClientId) { match self.connected_clients.iter().next() { Some(first_client_id) => { let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap(); self.connected_clients.insert(client_id); self.active_panes.insert(client_id, first_active_pane_id); + self.mode_info + .insert(client_id, self.default_mode_info.clone()); } None => { let mut pane_ids: Vec = self.panes.keys().copied().collect(); @@ -482,15 +501,32 @@ impl Tab { let first_pane_id = pane_ids.get(0).unwrap(); self.connected_clients.insert(client_id); self.active_panes.insert(client_id, *first_pane_id); + self.mode_info + .insert(client_id, self.default_mode_info.clone()); } } // TODO: we might be able to avoid this, we do this so that newly connected clients will // necessarily get a full render self.set_force_render(); } - pub fn add_multiple_clients(&mut self, client_ids: &[ClientId]) { + pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) { + self.mode_info.insert(client_id, mode_info); + } + pub fn add_multiple_clients( + &mut self, + client_ids: Vec, + client_mode_infos: Vec<(ClientId, ModeInfo)>, + ) { for client_id in client_ids { - self.add_client(*client_id); + self.add_client(client_id); + } + for (client_id, client_mode_info) in client_mode_infos { + log::info!( + "add_multiple_clients: client_id: {:?}, mode: {:?}", + client_id, + client_mode_info.mode + ); + self.mode_info.insert(client_id, client_mode_info); } } pub fn remove_client(&mut self, client_id: ClientId) { @@ -498,8 +534,12 @@ impl Tab { self.active_panes.remove(&client_id); self.set_force_render(); } - pub fn drain_connected_clients(&mut self) -> Vec { - self.connected_clients.drain().collect() + pub fn drain_connected_clients(&mut self) -> (Vec, Vec<(ClientId, ModeInfo)>) { + let client_mode_info = self.mode_info.drain(); + ( + self.connected_clients.drain().collect(), + client_mode_info.collect(), + ) } pub fn new_pane(&mut self, pid: PaneId, client_id: Option) { self.close_down_to_max_terminals(); @@ -750,7 +790,7 @@ impl Tab { PaneId::Plugin(pid) => { for key in parse_keys(&input_bytes) { self.senders - .send_to_plugin(PluginInstruction::Update(Some(pid), Event::Key(key))) + .send_to_plugin(PluginInstruction::Update(Some(pid), None, Event::Key(key))) .unwrap() } } @@ -926,25 +966,35 @@ impl Tab { let mut client_id_to_boundaries: HashMap = HashMap::new(); self.hide_cursor_and_clear_display_as_needed(output); // render panes and their frames - for pane in self.panes.values_mut() { + for (kind, pane) in self.panes.iter_mut() { if !self.panes_to_hide.contains(&pane.pid()) { - let mut pane_contents_and_ui = PaneContentsAndUi::new( - pane, - output, - self.colors, - &self.active_panes, - self.mode_info.mode, - ); - pane_contents_and_ui.render_pane_contents_for_all_clients(); + let mut pane_contents_and_ui = + PaneContentsAndUi::new(pane, output, self.colors, &self.active_panes); + if let PaneId::Terminal(..) = kind { + pane_contents_and_ui.render_pane_contents_for_all_clients(); + } for &client_id in &self.connected_clients { + let client_mode = self + .mode_info + .get(&client_id) + .unwrap_or(&self.default_mode_info) + .mode; + if let PaneId::Plugin(..) = kind { + pane_contents_and_ui.render_pane_contents_for_client(client_id); + } if self.draw_pane_frames { - pane_contents_and_ui.render_pane_frame(client_id, self.session_is_mirrored); + pane_contents_and_ui.render_pane_frame( + client_id, + client_mode, + self.session_is_mirrored, + ); } else { let boundaries = client_id_to_boundaries .entry(client_id) .or_insert_with(|| Boundaries::new(self.viewport)); pane_contents_and_ui.render_pane_boundaries( client_id, + client_mode, boundaries, self.session_is_mirrored, ); @@ -3325,7 +3375,11 @@ impl Tab { if let Some(selected_text) = selected_text { self.write_selection_to_clipboard(&selected_text); self.senders - .send_to_plugin(PluginInstruction::Update(None, Event::CopyToClipboard)) + .send_to_plugin(PluginInstruction::Update( + None, + None, + Event::CopyToClipboard, + )) .unwrap(); } } @@ -3343,7 +3397,11 @@ impl Tab { .send_to_server(ServerInstruction::Render(Some(output))) .unwrap(); self.senders - .send_to_plugin(PluginInstruction::Update(None, Event::CopyToClipboard)) + .send_to_plugin(PluginInstruction::Update( + None, + None, + Event::CopyToClipboard, + )) .unwrap(); } fn is_inside_viewport(&self, pane_id: &PaneId) -> bool { @@ -3387,6 +3445,7 @@ impl Tab { self.senders .send_to_plugin(PluginInstruction::Update( Some(*pid), + None, Event::Visible(visible), )) .unwrap(); diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index 40272436..f2742f17 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -13,7 +13,6 @@ pub struct PaneContentsAndUi<'a> { colors: Palette, focused_clients: Vec, multiple_users_exist_in_session: bool, - mode: InputMode, // TODO: per client } impl<'a> PaneContentsAndUi<'a> { @@ -22,7 +21,6 @@ impl<'a> PaneContentsAndUi<'a> { output: &'a mut Output, colors: Palette, active_panes: &HashMap, - mode: InputMode, ) -> Self { let focused_clients: Vec = active_panes .iter() @@ -36,11 +34,10 @@ impl<'a> PaneContentsAndUi<'a> { colors, focused_clients, multiple_users_exist_in_session, - mode, } } pub fn render_pane_contents_for_all_clients(&mut self) { - if let Some(vte_output) = self.pane.render() { + if let Some(vte_output) = self.pane.render(None) { // FIXME: Use Termion for cursor and style clearing? self.output.push_str_to_all_clients(&format!( "\u{1b}[{};{}H\u{1b}[m{}", @@ -50,6 +47,20 @@ impl<'a> PaneContentsAndUi<'a> { )); } } + pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) { + if let Some(vte_output) = self.pane.render(Some(client_id)) { + // FIXME: Use Termion for cursor and style clearing? + self.output.push_to_client( + client_id, + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + vte_output + ), + ); + } + } pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) { let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let pane_focused_for_different_client = self @@ -79,7 +90,12 @@ impl<'a> PaneContentsAndUi<'a> { } } } - pub fn render_pane_frame(&mut self, client_id: ClientId, session_is_mirrored: bool) { + pub fn render_pane_frame( + &mut self, + client_id: ClientId, + client_mode: InputMode, + session_is_mirrored: bool, + ) { let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let other_focused_clients: Vec = self .focused_clients @@ -89,7 +105,7 @@ impl<'a> PaneContentsAndUi<'a> { .collect(); let pane_focused_for_differet_client = !other_focused_clients.is_empty(); - let frame_color = self.frame_color(client_id, self.mode, session_is_mirrored); + let frame_color = self.frame_color(client_id, client_mode, session_is_mirrored); let focused_client = if pane_focused_for_client_id { Some(client_id) } else if pane_focused_for_differet_client { @@ -132,10 +148,11 @@ impl<'a> PaneContentsAndUi<'a> { pub fn render_pane_boundaries( &self, client_id: ClientId, + client_mode: InputMode, boundaries: &mut Boundaries, session_is_mirrored: bool, ) { - let color = self.frame_color(client_id, self.mode, session_is_mirrored); + let color = self.frame_color(client_id, client_mode, session_is_mirrored); boundaries.add_rect(self.pane.as_ref(), color); } fn frame_color( diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index f2fd6923..426ae98b 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -106,6 +106,7 @@ fn create_new_tab(size: Size) -> Tab { LayoutTemplate::default().try_into().unwrap(), vec![1], index, + client_id, ); tab } diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index 0dd963ba..b7055da6 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -25,6 +25,7 @@ use crate::{ pty::{ClientOrTabIndex, PtyInstruction}, screen::ScreenInstruction, thread_bus::{Bus, ThreadSenders}, + ClientId, }; use zellij_utils::{ @@ -40,10 +41,12 @@ use zellij_utils::{ #[derive(Clone, Debug)] pub(crate) enum PluginInstruction { - Load(Sender, RunPlugin, usize), // tx_pid, plugin metadata, tab_index - Update(Option, Event), // Focused plugin / broadcast, event data - Render(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols - Unload(u32), + Load(Sender, RunPlugin, usize, ClientId), // tx_pid, plugin metadata, tab_index, client_ids + Update(Option, Option, Event), // Focused plugin / broadcast, client_id, event data + Render(Sender, u32, ClientId, usize, usize), // String buffer, plugin id, client_id, rows, cols + Unload(u32), // plugin_id + AddClient(ClientId), + RemoveClient(ClientId), Exit, } @@ -53,8 +56,10 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::Load(..) => PluginContext::Load, PluginInstruction::Update(..) => PluginContext::Update, PluginInstruction::Render(..) => PluginContext::Render, - PluginInstruction::Unload(_) => PluginContext::Unload, + PluginInstruction::Unload(..) => PluginContext::Unload, PluginInstruction::Exit => PluginContext::Exit, + PluginInstruction::AddClient(_) => PluginContext::AddClient, + PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, } } } @@ -67,6 +72,7 @@ pub(crate) struct PluginEnv { pub wasi_env: WasiEnv, pub subscriptions: Arc>>, pub tab_index: usize, + pub client_id: ClientId, plugin_own_data_dir: PathBuf, } @@ -80,38 +86,25 @@ pub(crate) fn wasm_thread_main( info!("Wasm main thread starts"); let mut plugin_id = 0; - let mut plugin_map = HashMap::new(); + let mut headless_plugins = HashMap::new(); + let mut plugin_map: HashMap<(u32, ClientId), (Instance, PluginEnv)> = HashMap::new(); // u32 => pid + let mut connected_clients: Vec = vec![]; let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); fs::create_dir_all(&plugin_global_data_dir).unwrap(); - for plugin in plugins.iter() { - if let PluginType::Headless = plugin.run { - let (instance, plugin_env) = start_plugin( - plugin_id, - plugin, - 0, - &bus, - &store, - &data_dir, - &plugin_global_data_dir, - ); - plugin_map.insert(plugin_id, (instance, plugin_env)); - plugin_id += 1; - } - } - loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { - PluginInstruction::Load(pid_tx, run, tab_index) => { + PluginInstruction::Load(pid_tx, run, tab_index, client_id) => { let plugin = plugins .get(&run) .unwrap_or_else(|| panic!("Plugin {:?} could not be resolved", run)); let (instance, plugin_env) = start_plugin( plugin_id, + client_id, &plugin, tab_index, &bus, @@ -120,16 +113,34 @@ pub(crate) fn wasm_thread_main( &plugin_global_data_dir, ); - plugin_map.insert(plugin_id, (instance, plugin_env)); + plugin_map.insert( + (plugin_id, client_id), + (instance.clone(), plugin_env.clone()), + ); + + // clone plugins for the rest of the client ids if they exist + for client_id in connected_clients.iter() { + let mut new_plugin_env = plugin_env.clone(); + new_plugin_env.client_id = *client_id; + let module = instance.module().clone(); + let wasi = new_plugin_env.wasi_env.import_object(&module).unwrap(); + let zellij = zellij_exports(&store, &new_plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + plugin_map.insert((plugin_id, *client_id), (instance, new_plugin_env)); + } pid_tx.send(plugin_id).unwrap(); plugin_id += 1; } - PluginInstruction::Update(pid, event) => { - for (&i, (instance, plugin_env)) in &plugin_map { + PluginInstruction::Update(pid, cid, event) => { + for (&(plugin_id, client_id), (instance, plugin_env)) in &plugin_map { let subs = plugin_env.subscriptions.lock().unwrap(); // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? let event_type = EventType::from_str(&event.to_string()).unwrap(); - if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { + if subs.contains(&event_type) + && ((pid.is_none() && cid.is_none()) + || (pid.is_none() && cid == Some(client_id)) + || (cid.is_none() && pid == Some(plugin_id))) + { let update = instance.exports.get_function("update").unwrap(); wasi_write_object(&plugin_env.wasi_env, &event); update.call(&[]).unwrap(); @@ -137,11 +148,11 @@ pub(crate) fn wasm_thread_main( } drop(bus.senders.send_to_screen(ScreenInstruction::Render)); } - PluginInstruction::Render(buf_tx, pid, rows, cols) => { + PluginInstruction::Render(buf_tx, pid, cid, rows, cols) => { if rows == 0 || cols == 0 { buf_tx.send(String::new()).unwrap(); } else { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + let (instance, plugin_env) = plugin_map.get(&(pid, cid)).unwrap(); let render = instance.exports.get_function("render").unwrap(); render @@ -154,7 +165,54 @@ pub(crate) fn wasm_thread_main( PluginInstruction::Unload(pid) => { info!("Bye from plugin {}", &pid); // TODO: remove plugin's own data directory - drop(plugin_map.remove(&pid)); + let ids_in_plugin_map: Vec<(u32, ClientId)> = plugin_map.keys().copied().collect(); + for (plugin_id, client_id) in ids_in_plugin_map { + if pid == plugin_id { + drop(plugin_map.remove(&(plugin_id, client_id))); + } + } + } + PluginInstruction::AddClient(client_id) => { + connected_clients.push(client_id); + let mut seen = HashSet::new(); + let mut new_plugins = HashMap::new(); + for (&(plugin_id, client_id), (instance, plugin_env)) in &plugin_map { + if seen.contains(&plugin_id) { + continue; + } else { + seen.insert(plugin_id); + let mut new_plugin_env = plugin_env.clone(); + new_plugin_env.client_id = client_id; + new_plugins.insert(plugin_id, (instance.module().clone(), new_plugin_env)); + } + } + for (plugin_id, (module, mut new_plugin_env)) in new_plugins.drain() { + let wasi = new_plugin_env.wasi_env.import_object(&module).unwrap(); + let zellij = zellij_exports(&store, &new_plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + plugin_map.insert((plugin_id, client_id), (instance, new_plugin_env)); + } + + // load headless plugins + for plugin in plugins.iter() { + if let PluginType::Headless = plugin.run { + let (instance, plugin_env) = start_plugin( + plugin_id, + client_id, + plugin, + 0, + &bus, + &store, + &data_dir, + &plugin_global_data_dir, + ); + headless_plugins.insert(plugin_id, (instance, plugin_env)); + plugin_id += 1; + } + } + } + PluginInstruction::RemoveClient(client_id) => { + connected_clients.retain(|c| c != &client_id); } PluginInstruction::Exit => break, } @@ -163,8 +221,10 @@ pub(crate) fn wasm_thread_main( fs::remove_dir_all(&plugin_global_data_dir).unwrap(); } +#[allow(clippy::too_many_arguments)] fn start_plugin( plugin_id: u32, + client_id: ClientId, plugin: &PluginConfig, tab_index: usize, bus: &Bus, @@ -224,6 +284,7 @@ fn start_plugin( let plugin_env = PluginEnv { plugin_id, + client_id, plugin, senders: bus.senders.clone(), wasi_env, @@ -346,6 +407,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { // But that's a lot of code, and this is a few lines: let send_plugin_instructions = plugin_env.senders.to_plugin.clone(); let update_target = Some(plugin_env.plugin_id); + let client_id = plugin_env.client_id; thread::spawn(move || { let start_time = Instant::now(); thread::sleep(Duration::from_secs_f64(secs)); @@ -357,6 +419,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { .unwrap() .send(PluginInstruction::Update( update_target, + Some(client_id), Event::Timer(elapsed_time), )) .unwrap(); diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index d5407dd1..f8b8def9 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -294,6 +294,8 @@ pub enum PluginContext { Render, Unload, Exit, + AddClient, + RemoveClient, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s.