From b1d228f55ab7d0c5c109e4973a7c1039a982b2c6 Mon Sep 17 00:00:00 2001 From: Myah Mitchell Date: Mon, 5 Oct 2026 04:15:45 -0500 Subject: [PATCH] fix(screen): keep sending events to suppressed plugin panes (#5680) * fix(screen): keep sending events to suppressed plugin panes A plugin that hides itself (hide_self, hide_pane_with_id or hide_slot) moves into the tab's suppressed panes. Event delivery only looked at the tiled and floating panes, so a hidden plugin stopped receiving ModeUpdate, TabUpdate, PaneUpdate and InputReceived, and could never learn that it should show itself again. Include suppressed plugin panes when updating input modes and when picking the plugins for a broadcast event. Render targeting and the client visible plugins report still use get_plugin_ids, so a suppressed plugin is not rendered. * docs(changelog): note suppressed plugins receiving events --- CHANGELOG.md | 1 + zellij-server/src/screen.rs | 4 +- zellij-server/src/tab/mod.rs | 16 +++++- zellij-server/src/tab/unit/tab_tests.rs | 29 ++++++++++ zellij-server/src/unit/screen_tests.rs | 71 +++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 3 deletions(-) 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)