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
This commit is contained in:
Myah Mitchell
2026-10-05 11:15:45 +02:00
committed by GitHub
parent 0dac1906ad
commit b1d228f55a
5 changed files with 118 additions and 3 deletions
+1
View File
@@ -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)
+2 -2
View File
@@ -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
+15 -1
View File
@@ -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<PluginId> {
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<PaneInfo> {
let current_pane_group: HashMap<ClientId, Vec<PaneId>> =
{ self.current_pane_group.borrow().clone_inner() };
+29
View File
@@ -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));
}
+71
View File
@@ -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<u32> = vec![];
let mut tab_updates_received: Vec<u32> = 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)