diff --git a/Cargo.lock b/Cargo.lock index 6588cffb..779a9ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3000,6 +3000,15 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plugin-manager" +version = "0.1.0" +dependencies = [ + "fuzzy-matcher", + "uuid", + "zellij-tile", +] + [[package]] name = "polling" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 63a663fe..a90122ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "default-plugins/fixture-plugin-for-tests", "default-plugins/session-manager", "default-plugins/configuration", + "default-plugins/plugin-manager", "zellij-client", "zellij-server", "zellij-utils", diff --git a/default-plugins/configuration/src/main.rs b/default-plugins/configuration/src/main.rs index bcdc4d63..dee18b5f 100644 --- a/default-plugins/configuration/src/main.rs +++ b/default-plugins/configuration/src/main.rs @@ -1295,6 +1295,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Locked" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Locked" + }} }} shared_except "locked" "renametab" "renamepane" {{ bind "{primary_modifier} g" {{ SwitchToMode "Locked"; }} @@ -1475,6 +1482,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -1658,6 +1672,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -1830,6 +1851,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -2004,6 +2032,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -2162,6 +2197,13 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "p" {{ + LaunchOrFocusPlugin "plugin-manager" {{ + floating true + move_to_focused_tab true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 92935346..35f8d574 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -431,6 +431,20 @@ impl ZellijPlugin for State { should_change_focus_to_target_tab, ); }, + BareKey::Char('w') if key.has_modifiers(&[KeyModifier::Alt]) => { + reload_plugin_with_id(0); + }, + BareKey::Char('x') if key.has_modifiers(&[KeyModifier::Alt]) => { + let config = BTreeMap::new(); + let load_in_background = true; + let skip_plugin_cache = true; + load_new_plugin( + "zellij:OWN_URL", + config, + load_in_background, + skip_plugin_cache, + ) + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/plugin-manager/.cargo/config.toml b/default-plugins/plugin-manager/.cargo/config.toml new file mode 100644 index 00000000..6b77899c --- /dev/null +++ b/default-plugins/plugin-manager/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasi" diff --git a/default-plugins/plugin-manager/.gitignore b/default-plugins/plugin-manager/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/default-plugins/plugin-manager/.gitignore @@ -0,0 +1 @@ +/target diff --git a/default-plugins/plugin-manager/Cargo.toml b/default-plugins/plugin-manager/Cargo.toml new file mode 100644 index 00000000..71ef3626 --- /dev/null +++ b/default-plugins/plugin-manager/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "plugin-manager" +version = "0.1.0" +authors = ["Aram Drevekenin "] +edition = "2018" + +[dependencies] +uuid = { version = "1.7.0", features = ["v4"] } +fuzzy-matcher = "0.3.7" +zellij-tile = { path = "../../zellij-tile" } diff --git a/default-plugins/plugin-manager/LICENSE.md b/default-plugins/plugin-manager/LICENSE.md new file mode 120000 index 00000000..f0608a63 --- /dev/null +++ b/default-plugins/plugin-manager/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/default-plugins/plugin-manager/src/main.rs b/default-plugins/plugin-manager/src/main.rs new file mode 100644 index 00000000..bc451d8a --- /dev/null +++ b/default-plugins/plugin-manager/src/main.rs @@ -0,0 +1,1073 @@ +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use uuid::Uuid; +use zellij_tile::prelude::*; + +use std::collections::{BTreeMap, HashMap}; + +pub struct SearchResult { + plugin_id: u32, + plugin_info: PluginInfo, + indices: Vec, + score: i64, +} + +impl SearchResult { + pub fn new(plugin_id: u32, plugin_info: &PluginInfo, indices: Vec, score: i64) -> Self { + SearchResult { + plugin_id, + plugin_info: plugin_info.clone(), + indices, + score, + } + } +} + +pub struct NewPluginScreen { + new_plugin_url: String, + new_plugin_config: Vec<(String, String)>, // key/val for easy in-place manipulation + new_config_key: String, + new_config_val: String, + entering_plugin_url: bool, + entering_config_key: bool, + entering_config_val: bool, + selected_config_index: Option, + request_ids: Vec, + load_in_background: bool, +} + +impl Default for NewPluginScreen { + fn default() -> Self { + NewPluginScreen { + new_plugin_url: String::new(), + new_plugin_config: vec![], + new_config_key: String::new(), + new_config_val: String::new(), + entering_plugin_url: true, + entering_config_key: false, + entering_config_val: false, + selected_config_index: None, + request_ids: vec![], + load_in_background: true, + } + } +} + +impl NewPluginScreen { + pub fn render(&self, rows: usize, cols: usize) { + self.render_title(cols); + self.render_url_field(cols); + self.render_configuration_title(); + let config_list_len = self.render_config_list(cols, rows.saturating_sub(10)); // 10 - the rest + self.render_background_toggle(6 + config_list_len + 1); + if !self.editing_configuration() { + self.render_help(rows); + } + } + fn render_title(&self, cols: usize) { + let title_text = format!("LOAD NEW PLUGIN"); + let title_text_len = title_text.chars().count(); + let title = Text::new(title_text); + print_text_with_coordinates( + title, + (cols / 2).saturating_sub(title_text_len / 2), + 0, + None, + None, + ); + } + fn render_url_field(&self, cols: usize) { + let url_field = if self.entering_plugin_url { + let truncated_url = + truncate_string_start(&self.new_plugin_url, cols.saturating_sub(19)); // 17 the length of the prompt + 2 for padding and cursor + let text = format!("Enter Plugin URL: {}_", truncated_url); + Text::new(text).color_range(2, ..=16).color_range(3, 18..) + } else { + let truncated_url = + truncate_string_start(&self.new_plugin_url, cols.saturating_sub(18)); // 17 the length of the prompt + 1 for padding + let text = format!("Enter Plugin URL: {}", truncated_url); + Text::new(text).color_range(2, ..=16).color_range(0, 18..) + }; + print_text_with_coordinates(url_field, 0, 2, None, None); + let url_helper = + NestedListItem::new(format!(" - Load from Disk")).color_range(3, ..=8); + print_nested_list_with_coordinates(vec![url_helper], 0, 3, None, None); + } + fn render_configuration_title(&self) { + let configuration_title = + if !self.editing_configuration() && self.new_plugin_config.is_empty() { + Text::new(format!("Plugin Configuration: - Edit")) + .color_range(2, ..=20) + .color_range(3, 22..=26) + } else if !self.editing_configuration() { + Text::new(format!( + "Plugin Configuration: - Edit, <↓↑> - Navigate, - Delete" + )) + .color_range(2, ..=20) + .color_range(3, 22..=26) + .color_range(3, 36..=39) + .color_range(3, 53..=57) + } else { + Text::new(format!( + "Plugin Configuration: [Editing: - Next, - Accept]" + )) + .color_range(2, ..=20) + .color_range(3, 32..=36) + .color_range(3, 46..=52) + }; + print_text_with_coordinates(configuration_title, 0, 5, None, None); + } + fn editing_configuration(&self) -> bool { + self.entering_config_key || self.entering_config_val + } + fn render_config_list(&self, cols: usize, rows: usize) -> usize { + let mut items = vec![]; + let mut more_config_items = 0; + for (i, (config_key, config_val)) in self.new_plugin_config.iter().enumerate() { + let is_selected = Some(i) == self.selected_config_index; + if i >= rows { + more_config_items += 1; + } else if is_selected && self.editing_config_line() { + items.push(self.render_editing_config_line(config_key, config_val, cols)); + } else { + items.push(self.render_config_line(config_key, config_val, is_selected, cols)); + } + } + if self.editing_new_config_line() { + items.push(self.render_editing_config_line( + &self.new_config_key, + &self.new_config_val, + cols, + )); + } else if items.is_empty() { + items.push(NestedListItem::new("").color_range(0, ..)); + } + let config_list_len = items.len(); + print_nested_list_with_coordinates(items, 0, 6, Some(cols), None); + if more_config_items > 0 { + let more_text = format!("[+{}]", more_config_items); + print_text_with_coordinates( + Text::new(more_text).color_range(1, ..), + 0, + 6 + config_list_len, + None, + None, + ); + } + config_list_len + } + fn editing_config_line(&self) -> bool { + self.entering_config_key || self.entering_config_val + } + fn editing_new_config_line(&self) -> bool { + (self.entering_config_key || self.entering_config_val) + && self.selected_config_index.is_none() + } + fn render_editing_config_line( + &self, + config_key: &str, + config_val: &str, + config_line_max_len: usize, + ) -> NestedListItem { + let config_line_max_len = config_line_max_len.saturating_sub(6); // 3 - line padding, 1 - + // cursor, 2 ": " + let config_key_max_len = config_line_max_len / 2; + let config_val_max_len = config_line_max_len.saturating_sub(config_key_max_len); + let config_key = if config_key.chars().count() > config_key_max_len { + truncate_string_start(&config_key, config_key_max_len) + } else { + config_key.to_owned() + }; + let config_val = if config_val.chars().count() > config_val_max_len { + truncate_string_start(&config_val, config_val_max_len) + } else { + config_val.to_owned() + }; + if self.entering_config_key { + let val = if config_val.is_empty() { + "".to_owned() + } else { + config_val + }; + NestedListItem::new(format!("{}_: {}", config_key, val)) + .color_range(3, ..=config_key.chars().count()) + .color_range(1, config_key.chars().count() + 3..) + } else { + let key = if config_key.is_empty() { + "".to_owned() + } else { + config_key + }; + NestedListItem::new(format!("{}: {}_", key, config_val)) + .color_range(0, ..key.chars().count()) + .color_range(3, key.chars().count() + 2..) + } + } + fn render_config_line( + &self, + config_key: &str, + config_val: &str, + is_selected: bool, + config_line_max_len: usize, + ) -> NestedListItem { + let config_line_max_len = config_line_max_len.saturating_sub(5); // 3 - line padding, + // 2 - ": " + let config_key = if config_key.is_empty() { + "" + } else { + config_key + }; + let config_val = if config_val.is_empty() { + "" + } else { + config_val + }; + let config_key_max_len = config_line_max_len / 2; + let config_val_max_len = config_line_max_len.saturating_sub(config_key_max_len); + let config_key = if config_key.chars().count() > config_key_max_len { + truncate_string_start(&config_key, config_key_max_len) + } else { + config_key.to_owned() + }; + + let config_val = if config_val.chars().count() > config_val_max_len { + truncate_string_start(&config_val, config_val_max_len) + } else { + config_val.to_owned() + }; + let mut item = NestedListItem::new(format!("{}: {}", config_key, config_val)) + .color_range(0, ..config_key.chars().count()) + .color_range(1, config_key.chars().count() + 2..); + if is_selected { + item = item.selected() + } + item + } + fn render_background_toggle(&self, y_coordinates: usize) { + let key_shortcuts_text = format!("Ctrl l"); + print_text_with_coordinates( + Text::new(&key_shortcuts_text).color_range(3, ..), + 0, + y_coordinates, + None, + None, + ); + let load_in_background_text = format!("Load in Background"); + let load_in_foreground_text = format!("Load in Foreground"); + let (load_in_background_ribbon, load_in_foreground_ribbon) = if self.load_in_background { + ( + Text::new(&load_in_background_text).selected(), + Text::new(&load_in_foreground_text), + ) + } else { + ( + Text::new(&load_in_background_text), + Text::new(&load_in_foreground_text).selected(), + ) + }; + print_ribbon_with_coordinates( + load_in_background_ribbon, + key_shortcuts_text.chars().count() + 1, + y_coordinates, + None, + None, + ); + print_ribbon_with_coordinates( + load_in_foreground_ribbon, + key_shortcuts_text.chars().count() + 1 + load_in_background_text.chars().count() + 4, + y_coordinates, + None, + None, + ); + } + fn render_help(&self, rows: usize) { + let enter_line = Text::new(format!( + "Help: - Accept and Load Plugin, - Cancel" + )) + .color_range(3, 6..=12) + .color_range(3, 40..=44); + print_text_with_coordinates(enter_line, 0, rows, None, None); + } + fn get_field_being_edited_mut(&mut self) -> Option<&mut String> { + if self.entering_plugin_url { + Some(&mut self.new_plugin_url) + } else { + match self.selected_config_index { + Some(selected_config_index) => { + if self.entering_config_key { + self.new_plugin_config + .get_mut(selected_config_index) + .map(|(key, _val)| key) + } else if self.entering_config_val { + self.new_plugin_config + .get_mut(selected_config_index) + .map(|(_key, val)| val) + } else { + None + } + }, + None => { + if self.entering_config_key { + Some(&mut self.new_config_key) + } else if self.entering_config_val { + Some(&mut self.new_config_val) + } else { + None + } + }, + } + } + } + fn add_edit_buffer_to_config(&mut self) { + if !self.new_config_key.is_empty() || !self.new_config_val.is_empty() { + self.new_plugin_config.push(( + self.new_config_key.drain(..).collect(), + self.new_config_val.drain(..).collect(), + )); + } + } + pub fn handle_key(&mut self, key: KeyWithModifier) -> (bool, bool) { + let (mut should_render, mut should_close) = (false, false); + + match key.bare_key { + BareKey::Char(character) if key.has_no_modifiers() && character != ' ' => { + if let Some(field) = self.get_field_being_edited_mut() { + field.push(character); + } + should_render = true; + }, + BareKey::Backspace if key.has_no_modifiers() => { + if let Some(field) = self.get_field_being_edited_mut() { + field.pop(); + } + should_render = true; + }, + BareKey::Enter if key.has_no_modifiers() => { + if self.editing_configuration() { + self.add_edit_buffer_to_config(); + self.entering_config_key = false; + self.entering_config_val = false; + self.entering_plugin_url = true; + should_render = true; + } else { + let plugin_url: String = self.new_plugin_url.drain(..).collect(); + self.add_edit_buffer_to_config(); + let config = self.new_plugin_config.drain(..).into_iter().collect(); + let load_in_background = self.load_in_background; + let skip_plugin_cache = true; + load_new_plugin(plugin_url, config, load_in_background, skip_plugin_cache); + should_render = true; + should_close = true; + } + }, + BareKey::Tab if key.has_no_modifiers() => { + if self.entering_plugin_url { + self.entering_plugin_url = false; + self.entering_config_key = true; + } else if self.entering_config_key { + self.entering_config_key = false; + self.entering_config_val = true; + } else if self.entering_config_val { + self.entering_config_val = false; + if self.selected_config_index.is_none() { + // new config, add it to the map + self.add_edit_buffer_to_config(); + self.entering_config_key = true; + } else { + self.entering_plugin_url = true; + } + self.selected_config_index = None; + } else if self.selected_config_index.is_some() { + self.entering_config_key = true; + } else { + self.entering_plugin_url = true; + } + should_render = true; + }, + BareKey::Esc if key.has_no_modifiers() => { + if self.entering_config_key + || self.entering_config_val + || self.selected_config_index.is_some() + { + self.entering_plugin_url = true; + self.entering_config_key = false; + self.entering_config_val = false; + self.selected_config_index = None; + self.add_edit_buffer_to_config(); + should_render = true; + } else { + should_close = true; + } + }, + BareKey::Down if key.has_no_modifiers() => { + if !self.editing_configuration() { + let max_len = self.new_plugin_config.len().saturating_sub(1); + let has_config_values = !self.new_plugin_config.is_empty(); + if self.selected_config_index.is_none() && has_config_values { + self.selected_config_index = Some(0); + } else if self.selected_config_index == Some(max_len) { + self.selected_config_index = None; + } else { + self.selected_config_index = self.selected_config_index.map(|s| s + 1); + } + } + should_render = true; + }, + BareKey::Up if key.has_no_modifiers() => { + if !self.editing_configuration() { + let max_len = self.new_plugin_config.len().saturating_sub(1); + let has_config_values = !self.new_plugin_config.is_empty(); + if self.selected_config_index.is_none() && has_config_values { + self.selected_config_index = Some(max_len); + } else if self.selected_config_index == Some(0) { + self.selected_config_index = None; + } else { + self.selected_config_index = + self.selected_config_index.map(|s| s.saturating_sub(1)); + } + } + should_render = true; + }, + BareKey::Delete if key.has_no_modifiers() => { + if let Some(selected_config_index) = self.selected_config_index.take() { + self.new_plugin_config.remove(selected_config_index); + should_render = true; + } + }, + BareKey::Char('d') if key.has_modifiers(&[KeyModifier::Ctrl]) => { + let mut args = BTreeMap::new(); + let request_id = Uuid::new_v4(); + self.request_ids.push(request_id.to_string()); + let mut config = BTreeMap::new(); + config.insert("request_id".to_owned(), request_id.to_string()); + args.insert("request_id".to_owned(), request_id.to_string()); + pipe_message_to_plugin( + MessageToPlugin::new("filepicker") + .with_plugin_url("filepicker") + .with_plugin_config(config) + .new_plugin_instance_should_have_pane_title( + "Select a .wasm file to load as a plugin...", + ) + .with_args(args), + ); + }, + BareKey::Char('l') if key.has_modifiers(&[KeyModifier::Ctrl]) => { + self.load_in_background = !self.load_in_background; + should_render = true; + }, + _ => {}, + } + + (should_render, should_close) + } +} + +#[derive(Default)] +struct State { + userspace_configuration: BTreeMap, + plugins: BTreeMap, + search_results: Vec, + selected_index: Option, + expanded_indices: Vec, + tab_position_to_tab_name: HashMap, + plugin_id_to_tab_position: HashMap, + search_term: String, + new_plugin_screen: Option, +} + +register_plugin!(State); + +impl ZellijPlugin for State { + fn load(&mut self, configuration: BTreeMap) { + self.userspace_configuration = configuration; + subscribe(&[ + EventType::ModeUpdate, + EventType::PaneUpdate, + EventType::TabUpdate, + EventType::Key, + EventType::SessionUpdate, + EventType::PermissionRequestResult, + ]); + } + fn pipe(&mut self, pipe_message: PipeMessage) -> bool { + if pipe_message.name == "filepicker_result" { + match (pipe_message.payload, pipe_message.args.get("request_id")) { + (Some(payload), Some(request_id)) => { + match self + .new_plugin_screen + .as_mut() + .and_then(|n| n.request_ids.iter().position(|p| p == request_id)) + { + Some(request_id_position) => { + self.new_plugin_screen + .as_mut() + .map(|n| n.request_ids.remove(request_id_position)); + let chosen_plugin_location = std::path::PathBuf::from(payload); + self.new_plugin_screen.as_mut().map(|n| { + n.new_plugin_url = + format!("file:{}", chosen_plugin_location.display()) + }); + }, + None => { + eprintln!("request id not found"); + }, + } + }, + _ => {}, + } + true + } else { + false + } + } + fn update(&mut self, event: Event) -> bool { + let mut should_render = false; + match event { + Event::PermissionRequestResult(_) => { + let own_plugin_id = get_plugin_ids().plugin_id; + rename_plugin_pane(own_plugin_id, "Plugin Manager"); + should_render = true; + }, + Event::SessionUpdate(live_sessions, _dead_sessions) => { + for session in live_sessions { + if session.is_current_session { + if session.plugins != self.plugins { + self.plugins = session.plugins; + self.reset_selection(); + self.update_search_term(); + } + for tab in session.tabs { + self.tab_position_to_tab_name.insert(tab.position, tab.name); + } + } + } + should_render = true; + }, + Event::PaneUpdate(pane_manifest) => { + for (tab_position, panes) in pane_manifest.panes { + for pane_info in panes { + if pane_info.is_plugin { + self.plugin_id_to_tab_position + .insert(pane_info.id, tab_position); + } + } + } + }, + Event::Key(key) => match self.new_plugin_screen.as_mut() { + Some(new_plugin_screen) => { + let (should_render_new_plugin_screen, should_close_new_plugin_screen) = + new_plugin_screen.handle_key(key); + if should_close_new_plugin_screen { + self.new_plugin_screen = None; + should_render = true; + } else { + should_render = should_render_new_plugin_screen; + } + }, + None => should_render = self.handle_main_screen_key(key), + }, + _ => (), + }; + should_render + } + + fn render(&mut self, rows: usize, cols: usize) { + match &self.new_plugin_screen { + Some(new_plugin_screen) => { + new_plugin_screen.render(rows, cols); + }, + None => { + self.render_search(cols); + let list_y = 2; + let max_list_items = rows.saturating_sub(4); // 2 top padding, 2 bottom padding + let (selected_index_in_list, plugin_list) = if self.is_searching() { + self.render_search_results(cols) + } else { + self.render_plugin_list(cols) + }; + let (more_above, more_below, truncated_list) = self.truncate_list_to_screen( + selected_index_in_list, + plugin_list, + max_list_items, + ); + self.render_more_indication( + more_above, + more_below, + cols, + list_y, + truncated_list.len(), + ); + print_nested_list_with_coordinates(truncated_list, 0, list_y, Some(cols), None); + self.render_help(rows, cols); + }, + } + } +} + +impl State { + fn render_search_results(&self, cols: usize) -> (Option, Vec) { + let mut selected_index_in_list = None; + let mut plugin_list = vec![]; + for (i, search_result) in self.search_results.iter().enumerate() { + let is_selected = Some(i) == self.selected_index; + if is_selected { + selected_index_in_list = Some(plugin_list.len()); + } + let is_expanded = self.expanded_indices.contains(&i); + plugin_list.append(&mut self.render_search_result( + search_result, + is_selected, + is_expanded, + cols, + None, + )); + } + (selected_index_in_list, plugin_list) + } + fn render_plugin_list(&self, cols: usize) -> (Option, Vec) { + let mut selected_index_in_list = None; + let mut plugin_list = vec![]; + for (i, (plugin_id, plugin_info)) in self.plugins.iter().enumerate() { + let is_selected = Some(i) == self.selected_index; + let is_expanded = self.expanded_indices.contains(&i); + if is_selected { + selected_index_in_list = Some(plugin_list.len()); + } + plugin_list.append(&mut self.render_plugin( + *plugin_id, + plugin_info, + is_selected, + is_expanded, + cols, + )); + } + (selected_index_in_list, plugin_list) + } + fn render_more_indication( + &self, + more_above: usize, + more_below: usize, + cols: usize, + list_y: usize, + list_len: usize, + ) { + if more_above > 0 { + let text = format!("↑ [+{}]", more_above); + let text_len = text.chars().count(); + print_text_with_coordinates( + Text::new(text).color_range(1, ..), + cols.saturating_sub(text_len), + list_y.saturating_sub(1), + None, + None, + ); + } + if more_below > 0 { + let text = format!("↓ [+{}]", more_below); + let text_len = text.chars().count(); + print_text_with_coordinates( + Text::new(text).color_range(1, ..), + cols.saturating_sub(text_len), + list_y + list_len, + None, + None, + ); + } + } + pub fn render_search(&self, cols: usize) { + let text = format!(" SEARCH: {}_", self.search_term); + if text.chars().count() <= cols { + let text = Text::new(text).color_range(3, 9..); + print_text_with_coordinates(text, 0, 0, None, None); + } else { + let truncated_search_term = + truncate_string_start(&self.search_term, cols.saturating_sub(10)); // 9 the length of the SEARCH prompt + 1 for the cursor + let text = format!(" SEARCH: {}_", truncated_search_term); + let text = Text::new(text).color_range(3, 9..); + print_text_with_coordinates(text, 0, 0, None, None); + } + } + pub fn render_plugin( + &self, + plugin_id: u32, + plugin_info: &PluginInfo, + is_selected: bool, + is_expanded: bool, + cols: usize, + ) -> Vec { + let mut items = vec![]; + let plugin_location_len = plugin_info.location.chars().count(); + let max_location_len = cols.saturating_sub(3); // 3 for the bulletin + let location_string = if plugin_location_len > max_location_len { + truncate_string_start(&plugin_info.location, max_location_len) + } else { + plugin_info.location.clone() + }; + let mut item = self.render_plugin_line(location_string, None); + if is_selected { + item = item.selected(); + } + items.push(item); + if is_expanded { + let tab_line = self.render_tab_line(plugin_id, cols); + items.push(tab_line); + if !plugin_info.configuration.is_empty() { + let config_line = NestedListItem::new(format!("Configuration:")) + .color_range(2, ..=13) + .indent(1); + items.push(config_line); + for (config_key, config_val) in &plugin_info.configuration { + items.push(self.render_config_line(config_key, config_val, cols)) + } + } + } + items + } + fn render_config_line( + &self, + config_key: &str, + config_val: &str, + cols: usize, + ) -> NestedListItem { + let config_line_padding = 9; // 7, left padding + 2 for the ": " between key/val + let config_line_max_len = cols.saturating_sub(config_line_padding); + let config_key_max_len = config_line_max_len / 2; + let config_val_max_len = config_line_max_len.saturating_sub(config_key_max_len); + let config_key = if config_key.chars().count() > config_key_max_len { + truncate_string_start(&config_key, config_key_max_len) + } else { + config_key.to_owned() + }; + + let config_val = if config_val.chars().count() > config_val_max_len { + truncate_string_start(&config_val, config_val_max_len) + } else { + config_val.to_owned() + }; + NestedListItem::new(format!("{}: {}", config_key, config_val)) + .indent(2) + .color_range(0, ..config_key.chars().count()) + .color_range(1, config_key.chars().count() + 2..) + } + pub fn render_search_result( + &self, + search_result: &SearchResult, + is_selected: bool, + is_expanded: bool, + cols: usize, + plus_indication: Option, + ) -> Vec { + let mut items = vec![]; + let plugin_info = &search_result.plugin_info; + let plugin_id = search_result.plugin_id; + let indices = &search_result.indices; + let plus_indication_len = plus_indication + .map(|p| p.to_string().chars().count() + 4) + .unwrap_or(0); // 4 for the plus indication decorators and space + let max_location_len = cols.saturating_sub(plus_indication_len + 3); // 3 for the bulletin + let (location_string, indices) = if plugin_info.location.chars().count() <= max_location_len + { + (plugin_info.location.clone(), indices.clone()) + } else { + truncate_search_result(&plugin_info.location, max_location_len, indices) + }; + let mut item = match plus_indication { + Some(plus_indication) => self.render_plugin_line_with_plus_indication( + location_string, + plus_indication, + Some(indices), + ), + None => self.render_plugin_line(location_string, Some(indices)), + }; + if is_selected { + item = item.selected(); + } + items.push(item); + if is_expanded { + let tab_line = self.render_tab_line(plugin_id, cols); + items.push(tab_line); + if !plugin_info.configuration.is_empty() { + let config_line = NestedListItem::new(format!("Configuration:")) + .color_range(2, ..=13) + .indent(1); + items.push(config_line); + for (config_key, config_value) in &plugin_info.configuration { + items.push(self.render_config_line(config_key, config_value, cols)) + } + } + } + items + } + fn render_plugin_line_with_plus_indication( + &self, + location_string: String, + plus_indication: usize, + indices: Option>, + ) -> NestedListItem { + let mut item = NestedListItem::new(&format!("{} [+{}]", location_string, plus_indication)) + .color_range(0, ..) + .color_range(1, location_string.chars().count() + 1..); + if let Some(indices) = indices { + item = item.color_indices(3, indices); + } + item + } + fn render_plugin_line( + &self, + location_string: String, + indices: Option>, + ) -> NestedListItem { + let mut item = NestedListItem::new(location_string).color_range(0, ..); + if let Some(indices) = indices { + item = item.color_indices(3, indices); + } + item + } + fn render_tab_line(&self, plugin_id: u32, max_width: usize) -> NestedListItem { + let tab_of_plugin_id = self + .get_tab_of_plugin_id(plugin_id) + .unwrap_or_else(|| "N/A".to_owned()); + let tab_line_padding_count = 10; // 5 the length of the "Tab: " + 5 for the left padding + + let tab_of_plugin_id = + if tab_of_plugin_id.chars().count() + tab_line_padding_count > max_width { + truncate_string_start( + &tab_of_plugin_id, + max_width.saturating_sub(tab_line_padding_count), + ) + } else { + tab_of_plugin_id + }; + + let tab_line = NestedListItem::new(format!("Tab: {}", tab_of_plugin_id)) + .color_range(2, ..=3) + .indent(1); + tab_line + } + pub fn render_help(&self, y: usize, cols: usize) { + let full_text = "Help: <←↓↑→> - Navigate/Expand, - focus, - Reload, - Close, - New"; + let middle_text = + "Help: <←↓↑→/ENTER> - Navigate, - Reload, - Close, - New"; + let short_text = "<←↓↑→/ENTER/TAB/Del> - Navigate/Expand/Reload/Close, - New"; + if cols >= full_text.chars().count() { + let text = Text::new(full_text) + .color_range(3, 5..=11) + .color_range(3, 32..=38) + .color_range(3, 49..=53) + .color_range(3, 65..=69) + .color_range(3, 80..=87); + print_text_with_coordinates(text, 0, y, Some(cols), None); + } else if cols >= middle_text.chars().count() { + let text = Text::new(middle_text) + .color_range(3, 6..=17) + .color_range(3, 31..=35) + .color_range(3, 47..=51) + .color_range(3, 62..=69); + print_text_with_coordinates(text, 0, y, Some(cols), None); + } else { + let text = Text::new(short_text) + .color_range(3, ..=21) + .color_range(3, 53..=60); + print_text_with_coordinates(text, 0, y, Some(cols), None); + } + } + pub fn selected_plugin_id(&self) -> Option { + if self.is_searching() { + self.selected_index + .and_then(|i| self.search_results.iter().nth(i)) + .map(|search_result| search_result.plugin_id) + } else { + self.selected_index + .and_then(|i| self.plugins.iter().nth(i)) + .map(|(id, _)| *id) + } + } + pub fn focus_selected(&self) { + if let Some(selected_plugin_id) = self.selected_plugin_id() { + focus_pane_with_id(PaneId::Plugin(selected_plugin_id), true); + } + } + pub fn reload_selected(&self) { + if let Some(selected_plugin_id) = self.selected_plugin_id() { + reload_plugin_with_id(selected_plugin_id); + } + } + pub fn close_selected(&self) { + if let Some(selected_plugin_id) = self.selected_plugin_id() { + close_plugin_pane(selected_plugin_id); + } + } + pub fn reset_selection(&mut self) { + self.selected_index = None; + self.expanded_indices.clear(); + } + pub fn expand_selected(&mut self) { + if let Some(selected_index) = &self.selected_index { + self.expanded_indices.push(*selected_index); + } + } + pub fn collapse_selected(&mut self) { + if let Some(selected_index) = &self.selected_index { + self.expanded_indices.retain(|i| i != selected_index); + } + } + pub fn get_tab_of_plugin_id(&self, plugin_id: u32) -> Option { + self.plugin_id_to_tab_position + .get(&plugin_id) + .and_then(|plugin_id| self.tab_position_to_tab_name.get(plugin_id)) + .cloned() + } + pub fn update_search_term(&mut self) { + if self.search_term.is_empty() { + self.search_results.clear(); + } else { + let mut matches = vec![]; + let matcher = SkimMatcherV2::default().use_cache(true); + for (plugin_id, plugin_info) in &self.plugins { + if let Some((score, indices)) = + matcher.fuzzy_indices(&plugin_info.location, &self.search_term) + { + matches.push(SearchResult::new(*plugin_id, plugin_info, indices, score)); + } + } + matches.sort_by(|a, b| b.score.cmp(&a.score)); + self.search_results = matches; + } + } + pub fn handle_main_screen_key(&mut self, key: KeyWithModifier) -> bool { + let mut should_render = false; + match key.bare_key { + BareKey::Char(character) if key.has_no_modifiers() && character != ' ' => { + self.search_term.push(character); + self.update_search_term(); + self.reset_selection(); + should_render = true; + }, + BareKey::Backspace if key.has_no_modifiers() => { + self.search_term.pop(); + self.update_search_term(); + self.reset_selection(); + should_render = true; + }, + BareKey::Down if key.has_no_modifiers() => { + let max_len = if self.is_searching() { + self.search_results.len().saturating_sub(1) + } else { + self.plugins.keys().len().saturating_sub(1) + }; + if self.selected_index.is_none() { + self.selected_index = Some(0); + } else if self.selected_index == Some(max_len) { + self.selected_index = None; + } else { + self.selected_index = self.selected_index.map(|s| s + 1); + } + should_render = true; + }, + BareKey::Up if key.has_no_modifiers() => { + let max_len = if self.is_searching() { + self.plugins.keys().len().saturating_sub(1) + } else { + self.search_results.len().saturating_sub(1) + }; + if self.selected_index.is_none() { + self.selected_index = Some(max_len); + } else if self.selected_index == Some(0) { + self.selected_index = None; + } else { + self.selected_index = self.selected_index.map(|s| s.saturating_sub(1)); + } + should_render = true; + }, + BareKey::Enter if key.has_no_modifiers() => { + self.focus_selected(); + }, + BareKey::Right if key.has_no_modifiers() => { + self.expand_selected(); + should_render = true; + }, + BareKey::Left if key.has_no_modifiers() => { + self.collapse_selected(); + should_render = true; + }, + BareKey::Tab if key.has_no_modifiers() => { + self.reload_selected(); + }, + BareKey::Insert if key.has_no_modifiers() => { + self.new_plugin_screen = Some(Default::default()); + should_render = true; + }, + BareKey::Delete if key.has_no_modifiers() => { + self.close_selected(); + }, + BareKey::Esc if key.has_no_modifiers() => { + self.search_term.clear(); + self.update_search_term(); + should_render = true; + }, + _ => {}, + } + should_render + } + pub fn is_searching(&self) -> bool { + self.search_term.len() > 0 + } + fn truncate_list_to_screen( + &self, + selected_index_in_list: Option, + mut plugin_list: Vec, + max_list_items: usize, + ) -> (usize, usize, Vec) { + let mut more_above = 0; + let mut more_below = 0; + if plugin_list.len() > max_list_items { + let anchor_line = selected_index_in_list.unwrap_or(0); + let list_start = anchor_line.saturating_sub(max_list_items / 2); + let list_end = (list_start + max_list_items).saturating_sub(1); + let mut to_render = vec![]; + for (i, item) in plugin_list.drain(..).enumerate() { + if i >= list_start && i < list_end { + to_render.push(item); + } else if i >= list_end { + more_below += 1; + } else if i < list_start { + more_above += 1; + } + } + plugin_list = to_render; + } + (more_above, more_below, plugin_list) + } +} + +fn truncate_string_start(string_to_truncate: &str, max_len: usize) -> String { + let mut truncated_string = string_to_truncate.to_owned(); + let count_to_remove = truncated_string.chars().count().saturating_sub(max_len) + 5; + if truncated_string.chars().count() > max_len { + truncated_string.replace_range(0..count_to_remove, "[...]"); + } + truncated_string +} + +fn truncate_search_result( + plugin_location: &str, + max_location_len: usize, + indices: &Vec, +) -> (String, Vec) { + let truncated_location = truncate_string_start(&plugin_location, max_location_len); + let truncated_count = plugin_location + .chars() + .count() + .saturating_sub(max_location_len); + let adjusted_indices = indices + .iter() + .filter_map(|i| { + if i.saturating_sub(truncated_count) >= 5 { + Some(i.saturating_sub(truncated_count)) + } else { + None + } + }) + .collect(); + (truncated_location, adjusted_indices) +} diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index 8d1227af..0a2c3fac 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -1266,6 +1266,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)]) -> Vec)]) -> Vec { + let mut matching = keymap.iter().find_map(|(key, acvec)| { + let has_match = acvec + .iter() + .find(|a| a.launches_plugin("plugin-manager")) + .is_some(); + if has_match { + Some(key.clone()) + } else { + None + } + }); + if let Some(matching) = matching.take() { + vec![matching] + } else { + vec![] + } +} + fn configuration_key(keymap: &[(KeyWithModifier, Vec)]) -> Vec { let mut matching = keymap.iter().find_map(|(key, acvec)| { let has_match = acvec diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 28bd7b3c..88effcea 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -37,6 +37,7 @@ lazy_static::lazy_static! { WorkspaceMember{crate_name: "default-plugins/fixture-plugin-for-tests", build: true}, WorkspaceMember{crate_name: "default-plugins/session-manager", build: true}, WorkspaceMember{crate_name: "default-plugins/configuration", build: true}, + WorkspaceMember{crate_name: "default-plugins/plugin-manager", build: true}, WorkspaceMember{crate_name: "zellij-utils", build: false}, WorkspaceMember{crate_name: "zellij-tile-utils", build: false}, WorkspaceMember{crate_name: "zellij-tile", build: false}, diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index d1bd0585..1f984536 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -5,6 +5,7 @@ use zellij_utils::consts::{ }; use zellij_utils::data::{Event, HttpVerb, SessionInfo}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; +use zellij_utils::input::layout::RunPlugin; use zellij_utils::surf::{ http::{Method, Url}, RequestBuilder, @@ -34,6 +35,7 @@ pub enum BackgroundJob { StopPluginLoadingAnimation(u32), // u32 - plugin_id ReadAllSessionInfosOnMachine, // u32 - plugin_id ReportSessionInfo(String, SessionInfo), // String - session name + ReportPluginList(BTreeMap), // String - session name ReportLayoutInfo((String, BTreeMap)), // BTreeMap RunCommand( PluginId, @@ -71,6 +73,7 @@ impl From<&BackgroundJob> for BackgroundJobContext { BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo, BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand, BackgroundJob::WebRequest(..) => BackgroundJobContext::WebRequest, + BackgroundJob::ReportPluginList(..) => BackgroundJobContext::ReportPluginList, BackgroundJob::Exit => BackgroundJobContext::Exit, } } @@ -91,6 +94,8 @@ pub(crate) fn background_jobs_main( let mut loading_plugins: HashMap> = HashMap::new(); // u32 - plugin_id let current_session_name = Arc::new(Mutex::new(String::default())); let current_session_info = Arc::new(Mutex::new(SessionInfo::default())); + let current_session_plugin_list: Arc>> = + Arc::new(Mutex::new(BTreeMap::new())); let current_session_layout = Arc::new(Mutex::new((String::new(), BTreeMap::new()))); let last_serialization_time = Arc::new(Mutex::new(Instant::now())); let serialization_interval = serialization_interval.map(|s| s * 1000); // convert to @@ -152,6 +157,9 @@ pub(crate) fn background_jobs_main( *current_session_name.lock().unwrap() = session_name; *current_session_info.lock().unwrap() = session_info; }, + BackgroundJob::ReportPluginList(plugin_list) => { + *current_session_plugin_list.lock().unwrap() = plugin_list; + }, BackgroundJob::ReportLayoutInfo(session_layout) => { *current_session_layout.lock().unwrap() = session_layout; }, @@ -168,6 +176,7 @@ pub(crate) fn background_jobs_main( let current_session_info = current_session_info.clone(); let current_session_name = current_session_name.clone(); let current_session_layout = current_session_layout.clone(); + let current_session_plugin_list = current_session_plugin_list.clone(); let last_serialization_time = last_serialization_time.clone(); async move { loop { @@ -183,8 +192,16 @@ pub(crate) fn background_jobs_main( current_session_layout, ); } - let session_infos_on_machine = + let mut session_infos_on_machine = read_other_live_session_states(¤t_session_name); + for (session_name, session_info) in session_infos_on_machine.iter_mut() + { + if session_name == ¤t_session_name { + let current_session_plugin_list = + current_session_plugin_list.lock().unwrap().clone(); + session_info.populate_plugin_list(current_session_plugin_list); + } + } let resurrectable_sessions = find_resurrectable_sessions(&session_infos_on_machine); let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos( diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index af2723a0..7f412d52 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -14,6 +14,7 @@ use std::{ }; use wasmtime::Engine; +use crate::background_jobs::BackgroundJob; use crate::panes::PaneId; use crate::screen::ScreenInstruction; use crate::session_layout_metadata::SessionLayoutMetadata; @@ -49,13 +50,14 @@ pub enum PluginInstruction { bool, // should be opened in place Option, // pane title RunPluginOrAlias, - usize, // tab index + Option, // tab index Option, // pane id to replace if this is to be opened "in-place" ClientId, Size, Option, // cwd bool, // skip cache ), + LoadBackgroundPlugin(RunPluginOrAlias, ClientId), Update(Vec<(Option, Option, Event)>), // Focused plugin / broadcast, client_id, event data Unload(PluginId), // plugin_id Reload( @@ -65,6 +67,7 @@ pub enum PluginInstruction { usize, // tab index Size, ), + ReloadPluginWithId(u32), Resize(PluginId, usize, usize), // plugin_id, columns, rows AddClient(ClientId), RemoveClient(ClientId), @@ -162,9 +165,11 @@ impl From<&PluginInstruction> for PluginContext { fn from(plugin_instruction: &PluginInstruction) -> Self { match *plugin_instruction { PluginInstruction::Load(..) => PluginContext::Load, + PluginInstruction::LoadBackgroundPlugin(..) => PluginContext::LoadBackgroundPlugin, PluginInstruction::Update(..) => PluginContext::Update, PluginInstruction::Unload(..) => PluginContext::Unload, PluginInstruction::Reload(..) => PluginContext::Reload, + PluginInstruction::ReloadPluginWithId(..) => PluginContext::ReloadPluginWithId, PluginInstruction::Resize(..) => PluginContext::Resize, PluginInstruction::Exit => PluginContext::Exit, PluginInstruction::AddClient(_) => PluginContext::AddClient, @@ -279,7 +284,7 @@ pub(crate) fn plugin_thread_main( let start_suppressed = false; match wasm_bridge.load_plugin( &run_plugin, - Some(tab_index), + tab_index, size, cwd.clone(), skip_cache, @@ -292,7 +297,7 @@ pub(crate) fn plugin_thread_main( should_be_open_in_place, run_plugin_or_alias, pane_title, - Some(tab_index), + tab_index, plugin_id, pane_id_to_replace, cwd, @@ -305,6 +310,15 @@ pub(crate) fn plugin_thread_main( }, } }, + PluginInstruction::LoadBackgroundPlugin(run_plugin_or_alias, client_id) => { + load_background_plugin( + run_plugin_or_alias, + &mut wasm_bridge, + &bus, + &plugin_aliases, + client_id, + ); + }, PluginInstruction::Update(updates) => { wasm_bridge.update_plugins(updates, shutdown_send.clone())?; }, @@ -379,6 +393,9 @@ pub(crate) fn plugin_thread_main( }, } }, + PluginInstruction::ReloadPluginWithId(plugin_id) => { + wasm_bridge.reload_plugin_with_id(plugin_id).non_fatal(); + }, PluginInstruction::Resize(pid, new_columns, new_rows) => { wasm_bridge.resize_plugin(pid, new_columns, new_rows, shutdown_send.clone())?; }, diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 41bdb7f6..663d2687 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -422,6 +422,8 @@ impl<'a> PluginLoader<'a> { rows: running_plugin.rows, cols: running_plugin.columns, }; + let keybinds = running_plugin.store.data().keybinds.clone(); + let default_mode = running_plugin.store.data().default_mode; let plugin_config = running_plugin.store.data().plugin.clone(); loading_indication.set_name(running_plugin.store.data().name()); PluginLoader::new( diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index c33a6735..3cda4fc1 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -3,7 +3,7 @@ use crate::plugins::PluginId; use bytes::Bytes; use std::io::Write; use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, path::PathBuf, sync::{Arc, Mutex}, }; @@ -263,6 +263,24 @@ impl PluginMap { } }) } + pub fn list_plugins(&self) -> BTreeMap { + let all_plugin_ids: HashSet = self + .all_plugin_ids() + .into_iter() + .map(|(plugin_id, _client_id)| plugin_id) + .collect(); + let mut plugin_ids_to_cmds: BTreeMap = BTreeMap::new(); + for plugin_id in all_plugin_ids { + let plugin_cmd = self.run_plugin_of_plugin_id(plugin_id); + match plugin_cmd { + Some(plugin_cmd) => { + plugin_ids_to_cmds.insert(plugin_id, plugin_cmd.clone()); + }, + None => log::error!("Plugin with id: {plugin_id} not found"), + } + } + plugin_ids_to_cmds + } } pub type Subscriptions = HashSet; diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 690ef516..630ae70b 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -708,7 +708,7 @@ pub fn load_new_plugin_from_hd() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -788,7 +788,7 @@ pub fn load_new_plugin_with_plugin_alias() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -864,7 +864,7 @@ pub fn plugin_workers() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -944,7 +944,7 @@ pub fn plugin_workers_persist_state() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1034,7 +1034,7 @@ pub fn can_subscribe_to_hd_events() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1112,7 +1112,7 @@ pub fn switch_to_mode_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1184,7 +1184,7 @@ pub fn switch_to_mode_plugin_command_permission_denied() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1256,7 +1256,7 @@ pub fn new_tabs_with_layout_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1342,7 +1342,7 @@ pub fn new_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1414,7 +1414,7 @@ pub fn go_to_next_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1485,7 +1485,7 @@ pub fn go_to_previous_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1556,7 +1556,7 @@ pub fn resize_focused_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1627,7 +1627,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1698,7 +1698,7 @@ pub fn focus_next_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1769,7 +1769,7 @@ pub fn focus_previous_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1840,7 +1840,7 @@ pub fn move_focus_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1911,7 +1911,7 @@ pub fn move_focus_or_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -1982,7 +1982,7 @@ pub fn edit_scrollback_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2053,7 +2053,7 @@ pub fn write_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2124,7 +2124,7 @@ pub fn write_chars_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2195,7 +2195,7 @@ pub fn toggle_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2266,7 +2266,7 @@ pub fn move_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2337,7 +2337,7 @@ pub fn move_pane_with_direction_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2409,7 +2409,7 @@ pub fn clear_screen_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2481,7 +2481,7 @@ pub fn scroll_up_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2552,7 +2552,7 @@ pub fn scroll_down_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2623,7 +2623,7 @@ pub fn scroll_to_top_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2694,7 +2694,7 @@ pub fn scroll_to_bottom_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2765,7 +2765,7 @@ pub fn page_scroll_up_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2836,7 +2836,7 @@ pub fn page_scroll_down_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2907,7 +2907,7 @@ pub fn toggle_focus_fullscreen_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -2978,7 +2978,7 @@ pub fn toggle_pane_frames_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3049,7 +3049,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3120,7 +3120,7 @@ pub fn undo_rename_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3191,7 +3191,7 @@ pub fn close_focus_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3262,7 +3262,7 @@ pub fn toggle_active_tab_sync_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3333,7 +3333,7 @@ pub fn close_focused_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3404,7 +3404,7 @@ pub fn undo_rename_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3475,7 +3475,7 @@ pub fn previous_swap_layout_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3546,7 +3546,7 @@ pub fn next_swap_layout_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3617,7 +3617,7 @@ pub fn go_to_tab_name_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3688,7 +3688,7 @@ pub fn focus_or_create_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3759,7 +3759,7 @@ pub fn go_to_tab() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3830,7 +3830,7 @@ pub fn start_or_reload_plugin() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3908,7 +3908,7 @@ pub fn quit_zellij_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -3986,7 +3986,7 @@ pub fn detach_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4064,7 +4064,7 @@ pub fn open_file_floating_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4146,7 +4146,7 @@ pub fn open_file_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4229,7 +4229,7 @@ pub fn open_file_with_line_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4311,7 +4311,7 @@ pub fn open_file_with_line_floating_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4393,7 +4393,7 @@ pub fn open_terminal_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4471,7 +4471,7 @@ pub fn open_terminal_floating_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4549,7 +4549,7 @@ pub fn open_command_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4627,7 +4627,7 @@ pub fn open_command_pane_floating_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4698,7 +4698,7 @@ pub fn switch_to_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4769,7 +4769,7 @@ pub fn hide_self_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4839,7 +4839,7 @@ pub fn show_self_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4910,7 +4910,7 @@ pub fn close_terminal_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -4981,7 +4981,7 @@ pub fn close_plugin_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5052,7 +5052,7 @@ pub fn focus_terminal_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5123,7 +5123,7 @@ pub fn focus_plugin_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5194,7 +5194,7 @@ pub fn rename_terminal_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5265,7 +5265,7 @@ pub fn rename_plugin_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5336,7 +5336,7 @@ pub fn rename_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5416,7 +5416,7 @@ pub fn send_configuration_to_plugins() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5484,7 +5484,7 @@ pub fn request_plugin_permissions() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5576,7 +5576,7 @@ pub fn granted_permission_request_result() { false, plugin_title, run_plugin.clone(), - tab_index, + Some(tab_index), None, client_id, size, @@ -5667,7 +5667,7 @@ pub fn denied_permission_request_result() { false, plugin_title, run_plugin.clone(), - tab_index, + Some(tab_index), None, client_id, size, @@ -5738,7 +5738,7 @@ pub fn run_command_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5816,7 +5816,7 @@ pub fn run_command_with_env_vars_and_cwd_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5894,7 +5894,7 @@ pub fn web_request_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -5965,7 +5965,7 @@ pub fn unblock_input_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6047,7 +6047,7 @@ pub fn block_input_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6137,7 +6137,7 @@ pub fn pipe_output_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6220,7 +6220,7 @@ pub fn pipe_message_to_plugin_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6314,7 +6314,7 @@ pub fn switch_session_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6395,7 +6395,7 @@ pub fn switch_session_with_layout_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6476,7 +6476,7 @@ pub fn switch_session_with_layout_and_cwd_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6557,7 +6557,7 @@ pub fn disconnect_other_clients_plugins_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6638,7 +6638,7 @@ pub fn reconfigure_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6723,7 +6723,7 @@ pub fn run_plugin_in_specific_cwd() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6797,7 +6797,7 @@ pub fn hide_pane_with_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6868,7 +6868,7 @@ pub fn show_pane_with_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -6946,7 +6946,7 @@ pub fn open_command_pane_background_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7021,7 +7021,7 @@ pub fn rerun_command_pane_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7092,7 +7092,7 @@ pub fn resize_pane_with_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7163,7 +7163,7 @@ pub fn edit_scrollback_for_pane_with_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7234,7 +7234,7 @@ pub fn write_to_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7305,7 +7305,7 @@ pub fn write_chars_to_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7376,7 +7376,7 @@ pub fn move_pane_with_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7447,7 +7447,7 @@ pub fn move_pane_with_pane_id_in_direction_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7518,7 +7518,7 @@ pub fn clear_screen_for_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7589,7 +7589,7 @@ pub fn scroll_up_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7660,7 +7660,7 @@ pub fn scroll_down_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7731,7 +7731,7 @@ pub fn scroll_to_top_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7802,7 +7802,7 @@ pub fn scroll_to_bottom_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7873,7 +7873,7 @@ pub fn page_scroll_up_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -7944,7 +7944,7 @@ pub fn page_scroll_down_in_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8015,7 +8015,7 @@ pub fn toggle_pane_id_fullscreen_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8086,7 +8086,7 @@ pub fn toggle_pane_embed_or_eject_for_pane_id_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8157,7 +8157,7 @@ pub fn close_tab_with_index_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8228,7 +8228,7 @@ pub fn break_panes_to_new_tab_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8299,7 +8299,7 @@ pub fn break_panes_to_tab_with_index_plugin_command() { false, plugin_title, run_plugin, - tab_index, + Some(tab_index), None, client_id, size, @@ -8328,3 +8328,145 @@ pub fn break_panes_to_tab_with_index_plugin_command() { .clone(); assert_snapshot!(format!("{:#?}", screen_instruction)); } + +#[test] +#[ignore] +pub fn reload_plugin_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_naked_variant!( + received_screen_instructions, + ScreenInstruction::RequestStateUpdateForPlugins, // happens on successful plugin (re)load + screen_receiver, + 3, + &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('w')).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 request_state_update_requests = received_screen_instructions + .lock() + .unwrap() + .iter() + .filter(|i| { + if let ScreenInstruction::RequestStateUpdateForPlugins = i { + true + } else { + false + } + }) + .count(); + assert_eq!(request_state_update_requests, 3); +} + +#[test] +#[ignore] +pub fn load_new_plugin_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_naked_variant!( + received_screen_instructions, + ScreenInstruction::RequestStateUpdateForPlugins, // happens on successful plugin (re)load + screen_receiver, + 3, + &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('x')).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 request_state_update_requests = received_screen_instructions + .lock() + .unwrap() + .iter() + .filter(|i| { + if let ScreenInstruction::RequestStateUpdateForPlugins = i { + true + } else { + false + } + }) + .count(); + assert_eq!(request_state_update_requests, 3); +} diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 9e0583cf..799642e2 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -10,7 +10,7 @@ use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object}; use highway::{HighwayHash, PortableHash}; use log::info; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, path::PathBuf, str::FromStr, sync::{Arc, Mutex}, @@ -258,7 +258,7 @@ impl WasmBridge { plugin_cache, senders.clone(), engine, - plugin_map, + plugin_map.clone(), size, connected_clients.clone(), &mut loading_indication, @@ -273,7 +273,10 @@ impl WasmBridge { default_mode, keybinds, ) { - Ok(_) => handle_plugin_successful_loading(&senders, plugin_id), + Ok(_) => { + let plugin_list = plugin_map.lock().unwrap().list_plugins(); + handle_plugin_successful_loading(&senders, plugin_id, plugin_list); + }, Err(e) => handle_plugin_loading_failure( &senders, plugin_id, @@ -327,6 +330,84 @@ impl WasmBridge { .send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name)) .context("failed to unblock input pipe"); } + let plugin_list = plugin_map.list_plugins(); + let _ = self + .senders + .send_to_background_jobs(BackgroundJob::ReportPluginList(plugin_list)); + Ok(()) + } + pub fn reload_plugin_with_id(&mut self, plugin_id: u32) -> Result<()> { + let Some(run_plugin) = self.run_plugin_of_plugin_id(plugin_id).map(|r| r.clone()) else { + log::error!("Failed to find plugin with id: {}", plugin_id); + return Ok(()); + }; + + let (rows, columns) = self.size_of_plugin_id(plugin_id).unwrap_or((0, 0)); + self.cached_events_for_pending_plugins + .insert(plugin_id, vec![]); + self.cached_resizes_for_pending_plugins + .insert(plugin_id, (rows, columns)); + + let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string()); + self.start_plugin_loading_indication(&[plugin_id], &loading_indication); + let load_plugin_task = task::spawn({ + let plugin_dir = self.plugin_dir.clone(); + let plugin_cache = self.plugin_cache.clone(); + let senders = self.senders.clone(); + let engine = self.engine.clone(); + let plugin_map = self.plugin_map.clone(); + let connected_clients = self.connected_clients.clone(); + let path_to_default_shell = self.path_to_default_shell.clone(); + let zellij_cwd = self.zellij_cwd.clone(); + let capabilities = self.capabilities.clone(); + let client_attributes = self.client_attributes.clone(); + let default_shell = self.default_shell.clone(); + let default_layout = self.default_layout.clone(); + let layout_dir = self.layout_dir.clone(); + let base_modes = self.base_modes.clone(); + let keybinds = self.keybinds.clone(); + async move { + match PluginLoader::reload_plugin( + plugin_id, + plugin_dir.clone(), + plugin_cache.clone(), + senders.clone(), + engine.clone(), + plugin_map.clone(), + connected_clients.clone(), + &mut loading_indication, + path_to_default_shell.clone(), + zellij_cwd.clone(), + capabilities.clone(), + client_attributes.clone(), + default_shell.clone(), + default_layout.clone(), + layout_dir.clone(), + &base_modes, + &keybinds, + ) { + Ok(_) => { + let plugin_list = plugin_map.lock().unwrap().list_plugins(); + handle_plugin_successful_loading(&senders, plugin_id, plugin_list); + }, + Err(e) => { + handle_plugin_loading_failure( + &senders, + plugin_id, + &mut loading_indication, + &e, + None, + ); + }, + } + let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents { + plugin_ids: vec![plugin_id], + done_receiving_permissions: false, + }); + } + }); + self.loading_plugins + .insert((plugin_id, run_plugin.clone()), load_plugin_task); Ok(()) } pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> { @@ -386,7 +467,8 @@ impl WasmBridge { &keybinds, ) { Ok(_) => { - handle_plugin_successful_loading(&senders, first_plugin_id); + let plugin_list = plugin_map.lock().unwrap().list_plugins(); + handle_plugin_successful_loading(&senders, first_plugin_id, plugin_list); for plugin_id in &plugin_ids { if plugin_id == &first_plugin_id { // no need to reload the plugin we just reloaded @@ -412,7 +494,14 @@ impl WasmBridge { &base_modes, &keybinds, ) { - Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), + Ok(_) => { + let plugin_list = plugin_map.lock().unwrap().list_plugins(); + handle_plugin_successful_loading( + &senders, + *plugin_id, + plugin_list, + ); + }, Err(e) => handle_plugin_loading_failure( &senders, *plugin_id, @@ -1303,9 +1392,14 @@ impl WasmBridge { } } -fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) { +fn handle_plugin_successful_loading( + senders: &ThreadSenders, + plugin_id: PluginId, + plugin_list: BTreeMap, +) { let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id)); let _ = senders.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins); + let _ = senders.send_to_background_jobs(BackgroundJob::ReportPluginList(plugin_list)); } fn handle_plugin_loading_failure( diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index d1e938a2..82038c31 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -5,7 +5,7 @@ use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::pty::{ClientTabIndexOrPaneId, PtyInstruction}; use crate::route::route_action; use crate::ServerInstruction; -use log::{debug, warn}; +use log::warn; use serde::Serialize; use std::{ collections::{BTreeMap, HashSet}, @@ -339,6 +339,13 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) { tab_index, should_change_focus_to_new_tab, ), + PluginCommand::ReloadPlugin(plugin_id) => reload_plugin(env, plugin_id), + PluginCommand::LoadNewPlugin { + url, + config, + load_in_background, + skip_plugin_cache, + } => load_new_plugin(env, url, config, load_in_background, skip_plugin_cache), }, (PermissionStatus::Denied, permission) => { log::error!( @@ -1347,12 +1354,20 @@ fn close_terminal_pane(env: &PluginEnv, terminal_pane_id: u32) { let error_msg = || format!("failed to change tab focus in plugin {}", env.name()); let action = Action::CloseTerminalPane(terminal_pane_id); apply_action!(action, error_msg, env); + env.senders + .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal( + terminal_pane_id, + ))) + .non_fatal(); } fn close_plugin_pane(env: &PluginEnv, plugin_pane_id: u32) { let error_msg = || format!("failed to change tab focus in plugin {}", env.name()); let action = Action::ClosePluginPane(plugin_pane_id); apply_action!(action, error_msg, env); + env.senders + .send_to_plugin(PluginInstruction::Unload(plugin_pane_id)) + .non_fatal(); } fn focus_terminal_pane(env: &PluginEnv, terminal_pane_id: u32, should_float_if_hidden: bool) { @@ -1634,6 +1649,70 @@ fn break_panes_to_tab_with_index( }); } +fn reload_plugin(env: &PluginEnv, plugin_id: u32) { + let _ = env + .senders + .send_to_plugin(PluginInstruction::ReloadPluginWithId(plugin_id)); +} + +fn load_new_plugin( + env: &PluginEnv, + url: String, + config: BTreeMap, + load_in_background: bool, + skip_plugin_cache: bool, +) { + let url = if &url == "zellij:OWN_URL" { + env.plugin.location.display() + } else { + url + }; + if load_in_background { + match RunPluginOrAlias::from_url(&url, &Some(config), None, Some(env.plugin_cwd.clone())) { + Ok(run_plugin_or_alias) => { + let _ = env + .senders + .send_to_plugin(PluginInstruction::LoadBackgroundPlugin( + run_plugin_or_alias, + env.client_id, + )); + }, + Err(e) => { + log::error!("Failed to load new plugin: {:?}", e); + }, + } + } else { + let should_float = Some(true); + let should_be_open_in_place = false; + let pane_title = None; + let tab_index = None; + let pane_id_to_replace = None; + let client_id = env.client_id; + let size = Default::default(); + let cwd = Some(env.plugin_cwd.clone()); + let skip_cache = skip_plugin_cache; + match RunPluginOrAlias::from_url(&url, &Some(config), None, Some(env.plugin_cwd.clone())) { + Ok(run_plugin_or_alias) => { + let _ = env.senders.send_to_plugin(PluginInstruction::Load( + should_float, + should_be_open_in_place, + pane_title, + run_plugin_or_alias, + tab_index, + pane_id_to_replace, + client_id, + size, + cwd, + skip_cache, + )); + }, + Err(e) => { + log::error!("Failed to load new plugin: {:?}", e); + }, + } + } +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -1781,6 +1860,8 @@ fn check_command_permission( | PluginCommand::CloseTabWithIndex(..) | PluginCommand::BreakPanesToNewTab(..) | PluginCommand::BreakPanesToTabWithIndex(..) + | PluginCommand::ReloadPlugin(..) + | PluginCommand::LoadNewPlugin { .. } | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 92270943..47542dc4 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -1551,7 +1551,7 @@ impl Pty { should_open_in_place, pane_title, run, - tab_index, + Some(tab_index), pane_id_to_replace, client_id, size, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index e7b0cb7b..a4482807 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -1615,6 +1615,7 @@ impl Screen { connected_clients: self.active_tab_indices.keys().len(), is_current_session: true, available_layouts, + plugins: Default::default(), // these are filled in by the wasm thread }; self.bus .senders diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 268e10f2..8ede34c4 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1062,6 +1062,34 @@ pub fn break_panes_to_tab_with_index( unsafe { host_run_plugin_command() }; } +/// Reload an already-running in this session, optionally skipping the cache +pub fn reload_plugin_with_id(plugin_id: u32) { + let plugin_command = PluginCommand::ReloadPlugin(plugin_id); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + +/// Reload an already-running in this session, optionally skipping the cache +pub fn load_new_plugin>( + url: S, + config: BTreeMap, + load_in_background: bool, + skip_plugin_cache: bool, +) where + S: ToString, +{ + let plugin_command = PluginCommand::LoadNewPlugin { + url: url.to_string(), + config, + load_in_background, + skip_plugin_cache, + }; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + // Utility Functions #[allow(unused)] diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 2765c78e..e7dfcf5d 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -127,6 +127,13 @@ keybinds { }; SwitchToMode "Normal" } + bind "p" { + LaunchOrFocusPlugin "plugin-manager" { + floating true + move_to_focused_tab true + }; + SwitchToMode "Normal" + } } tmux { bind "[" { SwitchToMode "Scroll"; } @@ -208,6 +215,7 @@ plugins { cwd "/" } configuration location="zellij:configuration" + plugin-manager location="zellij:plugin-manager" } // Plugins to load in the background when a new session starts diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index e585f4ab..10c11343 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/configuration.wasm b/zellij-utils/assets/plugins/configuration.wasm index 581dbfb1..c4682e9d 100755 Binary files a/zellij-utils/assets/plugins/configuration.wasm and b/zellij-utils/assets/plugins/configuration.wasm differ diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm index bb260003..8de431e7 100755 Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ diff --git a/zellij-utils/assets/plugins/plugin-manager.wasm b/zellij-utils/assets/plugins/plugin-manager.wasm new file mode 100755 index 00000000..92bd8be7 Binary files /dev/null and b/zellij-utils/assets/plugins/plugin-manager.wasm differ diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm index 7d2fad73..4c5891ae 100755 Binary files a/zellij-utils/assets/plugins/session-manager.wasm and b/zellij-utils/assets/plugins/session-manager.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 4e882917..1e7ccc9a 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index a339363c..747d4330 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index afb73e8a..ae3cc734 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 2e9fafa7..79910d9b 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -270,6 +270,18 @@ pub struct SessionManifest { pub is_current_session: bool, #[prost(message, repeated, tag = "6")] pub available_layouts: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "7")] + pub plugins: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PluginInfo { + #[prost(uint32, tag = "1")] + pub plugin_id: u32, + #[prost(string, tag = "2")] + pub plugin_url: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub plugin_config: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 110e1669..19c146ac 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -5,7 +5,7 @@ pub struct PluginCommand { pub name: i32, #[prost( oneof = "plugin_command::Payload", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87" )] pub payload: ::core::option::Option, } @@ -168,10 +168,32 @@ pub mod plugin_command { BreakPanesToNewTabPayload(super::BreakPanesToNewTabPayload), #[prost(message, tag = "85")] BreakPanesToTabWithIndexPayload(super::BreakPanesToTabWithIndexPayload), + #[prost(message, tag = "86")] + ReloadPluginPayload(super::ReloadPluginPayload), + #[prost(message, tag = "87")] + LoadNewPluginPayload(super::LoadNewPluginPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct LoadNewPluginPayload { + #[prost(string, tag = "1")] + pub plugin_url: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "2")] + pub plugin_config: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "3")] + pub should_load_plugin_in_background: bool, + #[prost(bool, tag = "4")] + pub should_skip_plugin_cache: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReloadPluginPayload { + #[prost(uint32, tag = "1")] + pub plugin_id: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct BreakPanesToTabWithIndexPayload { #[prost(message, repeated, tag = "1")] pub pane_ids: ::prost::alloc::vec::Vec, @@ -660,6 +682,8 @@ pub enum CommandName { CloseTabWithIndex = 107, BreakPanesToNewTab = 108, BreakPanesToTabWithIndex = 109, + ReloadPlugin = 110, + LoadNewPlugin = 111, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -780,6 +804,8 @@ impl CommandName { CommandName::CloseTabWithIndex => "CloseTabWithIndex", CommandName::BreakPanesToNewTab => "BreakPanesToNewTab", CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex", + CommandName::ReloadPlugin => "ReloadPlugin", + CommandName::LoadNewPlugin => "LoadNewPlugin", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -897,6 +923,8 @@ impl CommandName { "CloseTabWithIndex" => Some(Self::CloseTabWithIndex), "BreakPanesToNewTab" => Some(Self::BreakPanesToNewTab), "BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex), + "ReloadPlugin" => Some(Self::ReloadPlugin), + "LoadNewPlugin" => Some(Self::LoadNewPlugin), _ => None, } } diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 1c11815c..66e0a8a8 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -111,6 +111,7 @@ mod not_wasm { add_plugin!(assets, "strider.wasm"); add_plugin!(assets, "session-manager.wasm"); add_plugin!(assets, "configuration.wasm"); + add_plugin!(assets, "plugin-manager.wasm"); assets }; } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index eb717945..8c2d2081 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1,7 +1,7 @@ use crate::input::actions::Action; use crate::input::config::ConversionError; use crate::input::keybinds::Keybinds; -use crate::input::layout::SplitSize; +use crate::input::layout::{RunPlugin, SplitSize}; use clap::ArgEnum; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; @@ -1199,6 +1199,22 @@ pub struct SessionInfo { pub connected_clients: usize, pub is_current_session: bool, pub available_layouts: Vec, + pub plugins: BTreeMap, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct PluginInfo { + pub location: String, + pub configuration: BTreeMap, +} + +impl From for PluginInfo { + fn from(run_plugin: RunPlugin) -> Self { + PluginInfo { + location: run_plugin.location.display(), + configuration: run_plugin.configuration.inner().clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -1253,6 +1269,14 @@ impl SessionInfo { pub fn update_connected_clients(&mut self, new_connected_clients: usize) { self.connected_clients = new_connected_clients; } + pub fn populate_plugin_list(&mut self, plugins: BTreeMap) { + // u32 - plugin_id + let mut plugin_list = BTreeMap::new(); + for (plugin_id, run_plugin) in plugins { + plugin_list.insert(plugin_id, run_plugin.into()); + } + self.plugins = plugin_list; + } } /// Contains all the information for a currently opened tab. @@ -1841,5 +1865,12 @@ pub enum PluginCommand { // Option - optional name for // the new tab BreakPanesToTabWithIndex(Vec, usize, bool), // usize - tab_index, bool - - // should_change_focus_to_new_tab + // should_change_focus_to_new_tab + ReloadPlugin(u32), // u32 - plugin pane id + LoadNewPlugin { + url: String, + config: BTreeMap, + load_in_background: bool, + skip_plugin_cache: bool, + }, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 64f21936..5f2cea0b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -402,10 +402,12 @@ pub enum PtyContext { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PluginContext { Load, + LoadBackgroundPlugin, Update, Render, Unload, Reload, + ReloadPluginWithId, Resize, Exit, AddClient, @@ -503,6 +505,7 @@ pub enum BackgroundJobContext { ReportLayoutInfo, RunCommand, WebRequest, + ReportPluginList, Exit, } diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index a646b386..e1e161d3 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -63,6 +63,7 @@ impl PluginConfig { || tag == "strider" || tag == "session-manager" || tag == "configuration" + || tag == "plugin-manager" { Some(PluginConfig { path: PathBuf::from(&tag), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index e57bb828..a46d69f5 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -4127,6 +4127,7 @@ impl SessionInfo { connected_clients, is_current_session, available_layouts, + plugins: Default::default(), // we do not serialize plugin information }) } pub fn to_string(&self) -> String { @@ -4662,6 +4663,7 @@ fn serialize_and_deserialize_session_info_with_data() { LayoutInfo::BuiltIn("layout2".to_owned()), LayoutInfo::File("layout3".to_owned()), ], + plugins: Default::default(), }; let serialized = session_info.to_string(); let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap(); diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap index a385211a..6a709ef9 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/kdl/mod.rs -assertion_line: 5525 +assertion_line: 5535 expression: fake_config_stringified --- keybinds clear-defaults=true { @@ -107,6 +107,13 @@ keybinds clear-defaults=true { SwitchToMode "normal" } bind "Ctrl o" { SwitchToMode "normal"; } + bind "p" { + LaunchOrFocusPlugin "plugin-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } bind "w" { LaunchOrFocusPlugin "session-manager" { floating true @@ -229,6 +236,7 @@ plugins { filepicker location="zellij:strider" { cwd "/" } + plugin-manager location="zellij:plugin-manager" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap index 3f438da1..b4bbbefe 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/kdl/mod.rs -assertion_line: 5537 +assertion_line: 5547 expression: fake_config_stringified --- keybinds clear-defaults=true { @@ -107,6 +107,13 @@ keybinds clear-defaults=true { SwitchToMode "normal" } bind "Ctrl o" { SwitchToMode "normal"; } + bind "p" { + LaunchOrFocusPlugin "plugin-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } bind "w" { LaunchOrFocusPlugin "session-manager" { floating true @@ -232,6 +239,7 @@ plugins { filepicker location="zellij:strider" { cwd "/" } + plugin-manager location="zellij:plugin-manager" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index a985806f..e3f3cacb 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -223,6 +223,13 @@ message SessionManifest { uint32 connected_clients = 4; bool is_current_session = 5; repeated LayoutInfo available_layouts = 6; + repeated PluginInfo plugins = 7; +} + +message PluginInfo { + uint32 plugin_id = 1; + string plugin_url = 2; + repeated ContextItem plugin_config = 3; } message LayoutInfo { diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index faeb53d2..d1e2f50d 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -8,7 +8,7 @@ pub use super::generated_api::api::{ LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload, PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, PaneType as ProtobufPaneType, - ResurrectableSession as ProtobufResurrectableSession, + PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession, SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *, }, input_mode::InputMode as ProtobufInputMode, @@ -19,13 +19,13 @@ pub use super::generated_api::api::{ use crate::data::{ CopyDestination, Event, EventType, FileMetadata, InputMode, KeyWithModifier, LayoutInfo, ModeInfo, Mouse, PaneId, PaneInfo, PaneManifest, PermissionStatus, PluginCapabilities, - SessionInfo, Style, TabInfo, + PluginInfo, SessionInfo, Style, TabInfo, }; use crate::errors::prelude::*; use crate::input::actions::Action; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::convert::TryFrom; use std::path::PathBuf; use std::time::Duration; @@ -667,10 +667,29 @@ impl TryFrom for ProtobufSessionManifest { .into_iter() .filter_map(|l| ProtobufLayoutInfo::try_from(l).ok()) .collect(), + plugins: session_info + .plugins + .into_iter() + .map(|p| ProtobufPluginInfo::from(p)) + .collect(), }) } } +impl From<(u32, PluginInfo)> for ProtobufPluginInfo { + fn from((plugin_id, plugin_info): (u32, PluginInfo)) -> ProtobufPluginInfo { + ProtobufPluginInfo { + plugin_id, + plugin_url: plugin_info.location, + plugin_config: plugin_info + .configuration + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(), + } + } +} + impl TryFrom for SessionInfo { type Error = &'static str; fn try_from(protobuf_session_manifest: ProtobufSessionManifest) -> Result { @@ -689,6 +708,20 @@ impl TryFrom for SessionInfo { let panes = PaneManifest { panes: pane_manifest, }; + let mut plugins = BTreeMap::new(); + for plugin_info in protobuf_session_manifest.plugins.into_iter() { + let mut configuration = BTreeMap::new(); + for context_item in plugin_info.plugin_config.into_iter() { + configuration.insert(context_item.name, context_item.value); + } + plugins.insert( + plugin_info.plugin_id, + PluginInfo { + location: plugin_info.plugin_url, + configuration, + }, + ); + } Ok(SessionInfo { name: protobuf_session_manifest.name, tabs: protobuf_session_manifest @@ -704,6 +737,7 @@ impl TryFrom for SessionInfo { .into_iter() .filter_map(|l| LayoutInfo::try_from(l).ok()) .collect(), + plugins, }) } } @@ -1702,6 +1736,16 @@ fn serialize_session_update_event_with_non_default_values() { }, ]; panes.insert(0, panes_list); + let mut plugins = BTreeMap::new(); + let mut plugin_configuration = BTreeMap::new(); + plugin_configuration.insert("config_key".to_owned(), "config_value".to_owned()); + plugins.insert( + 1, + PluginInfo { + location: "https://example.com/my-plugin.wasm".to_owned(), + configuration: plugin_configuration, + }, + ); let session_info_1 = SessionInfo { name: "session 1".to_owned(), tabs: tab_infos, @@ -1713,6 +1757,7 @@ fn serialize_session_update_event_with_non_default_values() { LayoutInfo::BuiltIn("layout2".to_owned()), LayoutInfo::File("layout3".to_owned()), ], + plugins, }; let session_info_2 = SessionInfo { name: "session 2".to_owned(), @@ -1727,6 +1772,7 @@ fn serialize_session_update_event_with_non_default_values() { LayoutInfo::BuiltIn("layout2".to_owned()), LayoutInfo::File("layout3".to_owned()), ], + plugins: Default::default(), }; let session_infos = vec![session_info_1, session_info_2]; let resurrectable_sessions = vec![]; diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 758d4e77..adb91e53 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -121,6 +121,8 @@ enum CommandName { CloseTabWithIndex = 107; BreakPanesToNewTab = 108; BreakPanesToTabWithIndex = 109; + ReloadPlugin = 110; + LoadNewPlugin = 111; } message PluginCommand { @@ -201,9 +203,22 @@ message PluginCommand { CloseTabWithIndexPayload close_tab_with_index_payload = 83; BreakPanesToNewTabPayload break_panes_to_new_tab_payload = 84; BreakPanesToTabWithIndexPayload break_panes_to_tab_with_index_payload = 85; + ReloadPluginPayload reload_plugin_payload = 86; + LoadNewPluginPayload load_new_plugin_payload = 87; } } +message LoadNewPluginPayload { + string plugin_url = 1; + repeated ContextItem plugin_config = 2; + bool should_load_plugin_in_background = 3; + bool should_skip_plugin_cache = 4; +} + +message ReloadPluginPayload { + uint32 plugin_id = 1; +} + message BreakPanesToTabWithIndexPayload { repeated PaneId pane_ids = 1; uint32 tab_index = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 15c56d03..d455500a 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -9,18 +9,19 @@ pub use super::generated_api::api::{ FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, HidePaneWithIdPayload, - HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload, MessageToPluginPayload, - MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload, - NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, + HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload, LoadNewPluginPayload, + MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, + MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, OpenCommandPanePayload, OpenFilePayload, PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ReconfigurePayload, - RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, - ResizePayload, RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, - ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetTimeoutPayload, - ShowPaneWithIdPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, - TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, - WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + ReloadPluginPayload, RequestPluginPermissionPayload, RerunCommandPanePayload, + ResizePaneIdWithDirectionPayload, ResizePayload, RunCommandPayload, + ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, + ScrollUpInPaneIdPayload, SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, + SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, + TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, + WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -1203,6 +1204,28 @@ impl TryFrom for PluginCommand { )), _ => Err("Mismatched payload for BreakPanesToTabWithIndex"), }, + Some(CommandName::ReloadPlugin) => match protobuf_plugin_command.payload { + Some(Payload::ReloadPluginPayload(reload_plugin_payload)) => { + Ok(PluginCommand::ReloadPlugin(reload_plugin_payload.plugin_id)) + }, + _ => Err("Mismatched payload for ReloadPlugin"), + }, + Some(CommandName::LoadNewPlugin) => match protobuf_plugin_command.payload { + Some(Payload::LoadNewPluginPayload(load_new_plugin_payload)) => { + Ok(PluginCommand::LoadNewPlugin { + url: load_new_plugin_payload.plugin_url, + config: load_new_plugin_payload + .plugin_config + .into_iter() + .map(|e| (e.name, e.value)) + .collect(), + load_in_background: load_new_plugin_payload + .should_load_plugin_in_background, + skip_plugin_cache: load_new_plugin_payload.should_skip_plugin_cache, + }) + }, + _ => Err("Mismatched payload for LoadNewPlugin"), + }, None => Err("Unrecognized plugin command"), } } @@ -1985,6 +2008,29 @@ impl TryFrom for ProtobufPluginCommand { }, )), }), + PluginCommand::ReloadPlugin(plugin_id) => Ok(ProtobufPluginCommand { + name: CommandName::ReloadPlugin as i32, + payload: Some(Payload::ReloadPluginPayload(ReloadPluginPayload { + plugin_id, + })), + }), + PluginCommand::LoadNewPlugin { + url, + config, + load_in_background, + skip_plugin_cache, + } => Ok(ProtobufPluginCommand { + name: CommandName::LoadNewPlugin as i32, + payload: Some(Payload::LoadNewPluginPayload(LoadNewPluginPayload { + plugin_url: url, + plugin_config: config + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(), + should_skip_plugin_cache: skip_plugin_cache, + should_load_plugin_in_background: load_in_background, + })), + }), } } } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index 07892df6..541a7196 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -4136,6 +4136,34 @@ Config { Right, ), ], + KeyWithModifier { + bare_key: Char( + 'p', + ), + key_modifiers: {}, + }: [ + LaunchOrFocusPlugin( + Alias( + PluginAlias { + name: "plugin-manager", + configuration: Some( + PluginUserConfiguration( + {}, + ), + ), + initial_cwd: None, + run_plugin: None, + }, + ), + true, + true, + false, + false, + ), + SwitchToMode( + Normal, + ), + ], KeyWithModifier { bare_key: Char( 'p', @@ -5611,6 +5639,18 @@ Config { "/", ), }, + "plugin-manager": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "plugin-manager", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 2d4200de..af80f720 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -4136,6 +4136,34 @@ Config { Right, ), ], + KeyWithModifier { + bare_key: Char( + 'p', + ), + key_modifiers: {}, + }: [ + LaunchOrFocusPlugin( + Alias( + PluginAlias { + name: "plugin-manager", + configuration: Some( + PluginUserConfiguration( + {}, + ), + ), + initial_cwd: None, + run_plugin: None, + }, + ), + true, + true, + false, + false, + ), + SwitchToMode( + Normal, + ), + ], KeyWithModifier { bare_key: Char( 'p', @@ -5611,6 +5639,18 @@ Config { "/", ), }, + "plugin-manager": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "plugin-manager", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap index b7b66e3a..72b3df8e 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap @@ -160,6 +160,18 @@ Config { "/", ), }, + "plugin-manager": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "plugin-manager", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index 87ccfb70..5c59e9dd 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -4136,6 +4136,34 @@ Config { Right, ), ], + KeyWithModifier { + bare_key: Char( + 'p', + ), + key_modifiers: {}, + }: [ + LaunchOrFocusPlugin( + Alias( + PluginAlias { + name: "plugin-manager", + configuration: Some( + PluginUserConfiguration( + {}, + ), + ), + initial_cwd: None, + run_plugin: None, + }, + ), + true, + true, + false, + false, + ), + SwitchToMode( + Normal, + ), + ], KeyWithModifier { bare_key: Char( 'p', @@ -5918,6 +5946,18 @@ Config { "/", ), }, + "plugin-manager": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "plugin-manager", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index 17f0e6d6..83edeb6c 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -4136,6 +4136,34 @@ Config { Right, ), ], + KeyWithModifier { + bare_key: Char( + 'p', + ), + key_modifiers: {}, + }: [ + LaunchOrFocusPlugin( + Alias( + PluginAlias { + name: "plugin-manager", + configuration: Some( + PluginUserConfiguration( + {}, + ), + ), + initial_cwd: None, + run_plugin: None, + }, + ), + true, + true, + false, + false, + ), + SwitchToMode( + Normal, + ), + ], KeyWithModifier { bare_key: Char( 'p', @@ -5611,6 +5639,18 @@ Config { "/", ), }, + "plugin-manager": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "plugin-manager", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij(