Skip to content

Commit

Permalink
feat(plugins): add API to list clients, their focused panes and runni…
Browse files Browse the repository at this point in the history
…ng commands/plugins (zellij-org#3687)

* fix(list-clients): properly show client info after a tab was closed

* feat(plugins): add API to list clients, their focused panes and running commands/plugins

* style(fmt): rustfmt
  • Loading branch information
imsnif committed Dec 31, 2024
1 parent d7d2cd1 commit 710b366
Show file tree
Hide file tree
Showing 17 changed files with 339 additions and 16 deletions.
3 changes: 3 additions & 0 deletions default-plugins/fixture-plugin-for-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@ impl ZellijPlugin for State {
];
rebind_keys(keys_to_unbind, keys_to_rebind, write_to_disk);
},
BareKey::Char('z') if key.has_modifiers(&[KeyModifier::Alt]) => {
list_clients();
},
_ => {},
},
Event::CustomMessage(message, payload) => {
Expand Down
33 changes: 32 additions & 1 deletion zellij-server/src/plugins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use wasm_bridge::WasmBridge;
use zellij_utils::{
async_std::{channel, future::timeout, task},
data::{
Event, EventType, InputMode, MessageToPlugin, PermissionStatus, PermissionType,
ClientInfo, Event, EventType, InputMode, MessageToPlugin, PermissionStatus, PermissionType,
PipeMessage, PipeSource, PluginCapabilities,
},
errors::{prelude::*, ContextType, PluginContext},
Expand Down Expand Up @@ -158,6 +158,7 @@ pub enum PluginInstruction {
file_path: Option<PathBuf>,
},
WatchFilesystem,
ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId),
Exit,
}

Expand Down Expand Up @@ -203,6 +204,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::FailedToWriteConfigToDisk { .. } => {
PluginContext::FailedToWriteConfigToDisk
},
PluginInstruction::ListClientsToPlugin(..) => PluginContext::ListClientsToPlugin,
}
}
}
Expand Down Expand Up @@ -607,6 +609,35 @@ pub(crate) fn plugin_thread_main(
},
}
},
PluginInstruction::ListClientsToPlugin(
mut session_layout_metadata,
plugin_id,
client_id,
) => {
populate_session_layout_metadata(
&mut session_layout_metadata,
&wasm_bridge,
&plugin_aliases,
);
let mut clients_metadata = session_layout_metadata.all_clients_metadata();
let mut client_list_for_plugin = vec![];
let default_editor = session_layout_metadata.default_editor.clone();
for (client_metadata_id, client_metadata) in clients_metadata.iter_mut() {
let is_current_client = client_metadata_id == &client_id;
client_list_for_plugin.push(ClientInfo::new(
*client_metadata_id,
client_metadata.get_pane_id().into(),
client_metadata.stringify_command(&default_editor),
is_current_client,
));
}
let updates = vec![(
Some(plugin_id),
Some(client_id),
Event::ListClients(client_list_for_plugin),
)];
wasm_bridge.update_plugins(updates, shutdown_send.clone())?;
},
PluginInstruction::LogLayoutToHd(mut session_layout_metadata) => {
populate_session_layout_metadata(
&mut session_layout_metadata,
Expand Down
71 changes: 71 additions & 0 deletions zellij-server/src/plugins/unit/plugin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8551,3 +8551,74 @@ pub fn rebind_keys_plugin_command() {
.clone();
assert_snapshot!(format!("{:#?}", rebind_event));
}

#[test]
#[ignore]
pub fn list_clients_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, screen_receiver, teardown) =
create_plugin_thread(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
..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!(
received_screen_instructions,
ScreenInstruction::ListClientsToPlugin,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);

let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
Some(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(KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier()), // this triggers the enent in the fixture plugin
)]));
screen_thread.join().unwrap(); // this might take a while if the cache is cold
teardown();
let list_clients_instruction = received_screen_instructions
.lock()
.unwrap()
.iter()
.find_map(|i| {
if let ScreenInstruction::ListClientsToPlugin(..) = i {
Some(i.clone())
} else {
None
}
})
.unwrap();
assert_snapshot!(format!("{:#?}", list_clients_instruction));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 8623
expression: "format!(\"{:#?}\", list_clients_instruction)"
---
ListClientsToPlugin(
0,
1,
)
14 changes: 13 additions & 1 deletion zellij-server/src/plugins/zellij_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) {
keys_to_unbind,
write_config_to_disk,
} => rebind_keys(env, keys_to_rebind, keys_to_unbind, write_config_to_disk)?,
PluginCommand::ListClients => list_clients(env),
},
(PermissionStatus::Denied, permission) => {
log::error!(
Expand Down Expand Up @@ -1474,6 +1475,15 @@ fn dump_session_layout(env: &PluginEnv) {
.map(|sender| sender.send(ScreenInstruction::DumpLayoutToPlugin(env.plugin_id)));
}

fn list_clients(env: &PluginEnv) {
let _ = env.senders.to_screen.as_ref().map(|sender| {
sender.send(ScreenInstruction::ListClientsToPlugin(
env.plugin_id,
env.client_id,
))
});
}

fn scan_host_folder(env: &PluginEnv, folder_to_scan: PathBuf) {
if !folder_to_scan.starts_with("/host") {
log::error!(
Expand Down Expand Up @@ -1897,7 +1907,9 @@ fn check_command_permission(
| PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState,
PluginCommand::ListClients | PluginCommand::DumpSessionLayout => {
PermissionType::ReadApplicationState
},
PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => {
PermissionType::Reconfigure
},
Expand Down
19 changes: 19 additions & 0 deletions zellij-server/src/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ pub enum PtyInstruction {
client_id: ClientId,
default_editor: Option<PathBuf>,
},
ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId),
Exit,
}

Expand All @@ -125,6 +126,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::FillPluginCwd(..) => PtyContext::FillPluginCwd,
PtyInstruction::ListClientsMetadata(..) => PtyContext::ListClientsMetadata,
PtyInstruction::Reconfigure { .. } => PtyContext::Reconfigure,
PtyInstruction::ListClientsToPlugin(..) => PtyContext::ListClientsToPlugin,
PtyInstruction::Exit => PtyContext::Exit,
}
}
Expand Down Expand Up @@ -724,6 +726,23 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
.with_context(err_context)
.non_fatal();
},
PtyInstruction::ListClientsToPlugin(
mut session_layout_metadata,
plugin_id,
client_id,
) => {
let err_context = || format!("Failed to dump layout");
pty.populate_session_layout_metadata(&mut session_layout_metadata);
pty.bus
.senders
.send_to_plugin(PluginInstruction::ListClientsToPlugin(
session_layout_metadata,
plugin_id,
client_id,
))
.with_context(err_context)
.non_fatal();
},
PtyInstruction::LogLayoutToHd(mut session_layout_metadata) => {
let err_context = || format!("Failed to dump layout");
pty.populate_session_layout_metadata(&mut session_layout_metadata);
Expand Down
19 changes: 18 additions & 1 deletion zellij-server/src/screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ pub enum ScreenInstruction {
should_change_focus_to_new_tab: bool,
client_id: ClientId,
},
ListClientsToPlugin(PluginId, ClientId),
}

impl From<&ScreenInstruction> for ScreenContext {
Expand Down Expand Up @@ -618,6 +619,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::BreakPanesToTabWithIndex { .. } => {
ScreenContext::BreakPanesToTabWithIndex
},
ScreenInstruction::ListClientsToPlugin(..) => ScreenContext::ListClientsToPlugin,
}
}
}
Expand Down Expand Up @@ -2503,7 +2505,7 @@ impl Screen {
let active_tab_index =
first_client_id.and_then(|client_id| self.active_tab_indices.get(&client_id));

for (tab_index, tab) in self.tabs.values().enumerate() {
for (tab_index, tab) in self.tabs.iter() {
let tab_is_focused = active_tab_index == Some(&tab_index);
let hide_floating_panes = !tab.are_floating_panes_visible();
let mut suppressed_panes = HashMap::new();
Expand Down Expand Up @@ -3107,6 +3109,21 @@ pub(crate) fn screen_thread_main(
.with_context(err_context)
.non_fatal();
},
ScreenInstruction::ListClientsToPlugin(plugin_id, client_id) => {
let err_context = || format!("Failed to dump layout");
let session_layout_metadata =
screen.get_layout_metadata(screen.default_shell.clone());
screen
.bus
.senders
.send_to_pty(PtyInstruction::ListClientsToPlugin(
session_layout_metadata,
plugin_id,
client_id,
))
.with_context(err_context)
.non_fatal();
},
ScreenInstruction::EditScrollback(client_id) => {
active_tab_and_connected_client_id!(
screen,
Expand Down
27 changes: 26 additions & 1 deletion zellij-server/src/session_layout_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,28 @@ impl SessionLayoutMetadata {

ClientMetadata::render_many(clients_metadata, &self.default_editor)
}
pub fn all_clients_metadata(&self) -> BTreeMap<ClientId, ClientMetadata> {
let mut clients_metadata: BTreeMap<ClientId, ClientMetadata> = BTreeMap::new();
for tab in &self.tabs {
let panes = if tab.hide_floating_panes {
&tab.tiled_panes
} else {
&tab.floating_panes
};
for pane in panes {
for focused_client in &pane.focused_clients {
clients_metadata.insert(
*focused_client,
ClientMetadata {
pane_id: pane.id.clone(),
command: pane.run.clone(),
},
);
}
}
}
clients_metadata
}
pub fn is_dirty(&self) -> bool {
// here we check to see if the serialized layout would be different than the base one, and
// thus is "dirty". A layout is considered dirty if one of the following is true:
Expand Down Expand Up @@ -372,7 +394,7 @@ impl PaneLayoutMetadata {
}
}

struct ClientMetadata {
pub struct ClientMetadata {
pane_id: PaneId,
command: Option<Run>,
}
Expand Down Expand Up @@ -404,6 +426,9 @@ impl ClientMetadata {
};
stringified.unwrap_or("N/A".to_owned())
}
pub fn get_pane_id(&self) -> PaneId {
self.pane_id
}
pub fn render_many(
clients_metadata: BTreeMap<ClientId, ClientMetadata>,
default_editor: &Option<PathBuf>,
Expand Down
9 changes: 9 additions & 0 deletions zellij-tile/src/shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,15 @@ pub fn dump_session_layout() {
unsafe { host_run_plugin_command() };
}

/// Get a list of clients, their focused pane and running command or focused plugin back as an
/// Event::ListClients (note: this event must be subscribed to)
pub fn list_clients() {
let plugin_command = PluginCommand::ListClients;
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}

/// Change configuration for the current user
pub fn reconfigure(new_config: String, save_configuration_file: bool) {
let plugin_command = PluginCommand::Reconfigure(new_config, save_configuration_file);
Expand Down
Loading

0 comments on commit 710b366

Please sign in to comment.