diff options
| author | Aram Drevekenin <aram@poor.dev> | 2024-09-30 18:04:40 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-09-30 18:04:40 +0200 |
| commit | 9f1e38f9fa6efd166f5205eaa8603d6b05a68b2b (patch) | |
| tree | 3e41bdf65e4bb83fc08f3ad23f913630a97a3d97 /default-plugins/plugin-manager/src | |
| parent | docs(changelog): background plugins (diff) | |
| download | sl0ppij-9f1e38f9fa6efd166f5205eaa8603d6b05a68b2b.tar.gz sl0ppij-9f1e38f9fa6efd166f5205eaa8603d6b05a68b2b.tar.xz sl0ppij-9f1e38f9fa6efd166f5205eaa8603d6b05a68b2b.zip | |
feat(ui): built-in plugin manager (#3633)
* add plugin list to session info
* feat(plugins): new_plugin and reload_plugin API commands
* feat(plugins): built-in plugin manager
* style(fmt): rustfmt
* update plugins
Diffstat (limited to 'default-plugins/plugin-manager/src')
| -rw-r--r-- | default-plugins/plugin-manager/src/main.rs | 1073 |
1 files changed, 1073 insertions, 0 deletions
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<usize>, + score: i64, +} + +impl SearchResult { + pub fn new(plugin_id: u32, plugin_info: &PluginInfo, indices: Vec<usize>, 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<usize>, + request_ids: Vec<String>, + 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!("<Ctrl d> - 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: <TAB> - Edit")) + .color_range(2, ..=20) + .color_range(3, 22..=26) + } else if !self.editing_configuration() { + Text::new(format!( + "Plugin Configuration: <TAB> - Edit, <↓↑> - Navigate, <Del> - 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: <TAB> - Next, <ENTER> - 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("<NO CONFIGURATION>").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() { + "<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() { + "<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() { + "<EMPTY>" + } else { + config_key + }; + let config_val = if config_val.is_empty() { + "<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: <ENTER> - Accept and Load Plugin, <ESC> - 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<String, String>, + plugins: BTreeMap<u32, PluginInfo>, + search_results: Vec<SearchResult>, + selected_index: Option<usize>, + expanded_indices: Vec<usize>, + tab_position_to_tab_name: HashMap<usize, String>, + plugin_id_to_tab_position: HashMap<u32, usize>, + search_term: String, + new_plugin_screen: Option<NewPluginScreen>, +} + +register_plugin!(State); + +impl ZellijPlugin for State { + fn load(&mut self, configuration: BTreeMap<String, String>) { + 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<usize>, Vec<NestedListItem>) { + 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<usize>, Vec<NestedListItem>) { + 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<NestedListItem> { + 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<usize>, + ) -> Vec<NestedListItem> { + 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<Vec<usize>>, + ) -> 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<Vec<usize>>, + ) -> 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, <ENTER> - focus, <TAB> - Reload, <Del> - Close, <INSERT> - New"; + let middle_text = + "Help: <←↓↑→/ENTER> - Navigate, <TAB> - Reload, <Del> - Close, <INSERT> - New"; + let short_text = "<←↓↑→/ENTER/TAB/Del> - Navigate/Expand/Reload/Close, <INSERT> - 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<u32> { + 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<String> { + 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<usize>, + mut plugin_list: Vec<NestedListItem>, + max_list_items: usize, + ) -> (usize, usize, Vec<NestedListItem>) { + 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<usize>, +) -> (String, Vec<usize>) { + 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) +} |
