diff --git a/CHANGELOG.md b/CHANGELOG.md index 03939c796..070bed625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix: add websocket heartbeat to webserver to prevernt idle disconnect through proxies (https://github.com/zellij-org/zellij/pull/4633) * fix: memory-usage optimizations and multiple-pane-single-instance-plugins (https://github.com/zellij-org/zellij/pull/5662) * fix: erasing wide CJK character correctness (https://github.com/zellij-org/zellij/pull/5657) +* fix: plugins hidden with `hide_self` keep receiving mode, tab and pane updates (https://github.com/zellij-org/zellij/pull/5680) ## [0.45.1] - 2026-08-28 * fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522) diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index d9d82a585..95ec93f72 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -6847,10 +6847,10 @@ impl Screen { }; } let mut plugin_ids = Vec::new(); - // Active-tab plugins + // Active-tab plugins, including suppressed ones so they can decide to show themselves if let Some(active_tab_id) = self.active_tab_ids.get(&client_id) { if let Some(tab) = self.tabs.get(active_tab_id) { - plugin_ids.extend(tab.get_plugin_ids()); + plugin_ids.extend(tab.get_plugin_ids_including_suppressed()); } } // Background plugins subscribed to this event type diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index a2a90e7d2..a437ce8c4 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -2109,7 +2109,7 @@ impl Tab { let mode_infos = self.mode_info.borrow(); let mut plugin_updates = vec![]; let currently_marking_pane_group = self.currently_marking_pane_group.borrow(); - let tab_plugin_ids = self.get_plugin_ids(); + let tab_plugin_ids = self.get_plugin_ids_including_suppressed(); for client_id in self.connected_clients.borrow().iter() { let mut mode_info = mode_infos .get(client_id) @@ -5660,6 +5660,20 @@ impl Tab { }) .collect() } + /// Like `get_plugin_ids`, but also includes plugin panes that are suppressed (eg. hidden + /// with `hide_self`). These are still running and should keep receiving events, otherwise + /// a hidden plugin cannot know when to show itself again. + pub fn get_plugin_ids_including_suppressed(&self) -> Vec { + let mut plugin_ids = self.get_plugin_ids(); + for pane_id in self.suppressed_panes.keys() { + if let PaneId::Plugin(plugin_id) = pane_id { + if !plugin_ids.contains(plugin_id) { + plugin_ids.push(*plugin_id); + } + } + } + plugin_ids + } pub fn get_pane_info(&self, pane_id: PaneId) -> Option { let current_pane_group: HashMap> = { self.current_pane_group.borrow().clone_inner() }; diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index e887d434f..dfbe1fb11 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -18150,3 +18150,32 @@ fn floating_plugin_panes_are_not_shown_again_when_their_tab_returns_with_the_sur "a plugin whose floating surface is hidden should not be told it is visible when its tab returns" ); } + +#[test] +fn suppressed_plugin_panes_get_mode_updates_but_are_not_visible() { + let (mut tab, plugin_receiver) = tab_with_floating_plugin_pane(1); + tab.suppress_pane(PaneId::Plugin(1), Some(1)); + while plugin_receiver.try_recv().is_ok() {} + + tab.update_input_modes().unwrap(); + + let mut got_mode_update = false; + while let Ok((instruction, _)) = plugin_receiver.try_recv() { + if let PluginInstruction::Update(updates) = instruction { + for (pid, _client_id, event) in updates { + if pid == Some(1) && matches!(event, Event::ModeUpdate(..)) { + got_mode_update = true; + } + } + } + } + assert!( + got_mode_update, + "a suppressed plugin should still receive ModeUpdate so it can show itself again" + ); + assert!( + !tab.get_plugin_ids().contains(&1), + "a suppressed plugin should not count as visible, since render targeting uses get_plugin_ids" + ); + assert!(tab.get_plugin_ids_including_suppressed().contains(&1)); +} diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index dacd74b36..62d99da5c 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -8290,6 +8290,77 @@ pub fn closing_tab_updates_input_modes_of_destination_tab_plugins() { ); } +#[test] +pub fn suppressed_plugin_keeps_receiving_events() { + // Tab 0: plugin panes 2 and 3, the client switches here and plugin 2 hides itself. + // It should still hear about mode and tab changes so it can decide to show itself again. + let size = Size { cols: 80, rows: 10 }; + let client_id = 10; + + let mut mock_screen = MockScreen::new(size); + mock_screen.new_tab_with_plugins(vec![2, 3]); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(None, vec![]); + + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver + ); + + let goto_tab = CliAction::GoToTab { index: 1 }; + send_cli_action_to_server(&session_metadata, goto_tab, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = mock_screen.to_screen.send(ScreenInstruction::SuppressPane( + PaneId::Plugin(2), + client_id, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + let instructions_before_mode_change = received_plugin_instructions.lock().unwrap().len(); + + let _ = mock_screen.to_screen.send(ScreenInstruction::ChangeMode( + InputMode::Locked, + None, + client_id, + None, + )); + let rename_tab = CliAction::RenameTab { + name: "renamed".to_owned(), + tab_id: None, + }; + send_cli_action_to_server(&session_metadata, rename_tab, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![plugin_thread, screen_thread]); + + let instructions = received_plugin_instructions.lock().unwrap(); + let mut mode_updates_received: Vec = vec![]; + let mut tab_updates_received: Vec = vec![]; + for instruction in instructions[instructions_before_mode_change..].iter() { + if let PluginInstruction::Update(updates) = instruction { + for (pid, _cid, event) in updates { + match (pid, event) { + (Some(pid), Event::ModeUpdate(..)) => mode_updates_received.push(*pid), + (Some(pid), Event::TabUpdate(..)) => tab_updates_received.push(*pid), + _ => {}, + } + } + } + } + assert!( + mode_updates_received.contains(&2), + "suppressed plugin 2 should receive a ModeUpdate, got: {:?}", + mode_updates_received + ); + assert!( + tab_updates_received.contains(&2), + "suppressed plugin 2 should receive a TabUpdate, got: {:?}", + tab_updates_received + ); +} + #[test] pub fn inactive_tab_plugins_get_fresh_state_on_activation() { // Tab 0: plugin pane 2 (from new_tab_with_plugins)