use std::collections::{BTreeSet, HashMap}; use std::time::Instant; use crate::output::{CharacterChunk, KittyImageChunk, SixelImageChunk}; use crate::panes::{ grid::Grid, kitty_graphics::KittyImageStore, sixel::SixelImageStore, terminal_pane::{BRACKETED_PASTE_BEGIN, BRACKETED_PASTE_END}, LinkHandler, PaneId, }; use crate::plugins::PluginInstruction; use crate::pty::VteBytes; use crate::tab::{AdjustedInput, Pane}; use crate::ui::{ loading_indication::LoadingIndication, pane_boundaries_frame::{gap_ring_chunks, FrameParams, PaneFrame}, }; use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; use vte; use zellij_utils::data::PaneContents; use zellij_utils::data::{ BareKey, KeyWithModifier, PermissionStatus, PermissionType, PluginPermission, }; use zellij_utils::pane_size::{Offset, SizeInPixels, Viewport}; use zellij_utils::position::Position; use zellij_utils::{ channels::SenderWithContext, data::{Event, InputMode, Mouse, Palette, PaletteColor, Style, Styling}, errors::prelude::*, input::layout::Run, input::mouse::{MouseEvent, MouseEventType}, pane_size::PaneGeom, shared::make_terminal_title, }; macro_rules! style { ($fg:expr) => { ansi_term::Style::new().fg(match $fg { PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b), PaletteColor::EightBit(color) => ansi_term::Color::Fixed(color), }) }; } macro_rules! get_or_create_grid { ($self:ident, $client_id:ident) => {{ let rows = $self.get_content_rows(); let cols = $self.get_content_columns(); let osc8_hyperlinks = true; // N/A for plugins, always enabled let explicitly_disable_kitty_keyboard_protocol = false; // N/A for plugins $self.grids.entry($client_id).or_insert_with(|| { let mut grid = Grid::new( rows, cols, $self.terminal_emulator_colors.clone(), $self.terminal_emulator_color_codes.clone(), $self.link_handler.clone(), $self.character_cell_size.clone(), $self.sixel_image_store.clone(), $self.kitty_image_store.clone(), $self.style.clone(), $self.debug, $self.arrow_fonts, $self.styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, ); grid.hide_cursor(); grid }) }}; } pub(crate) struct PluginPane { pub pid: u32, pub should_render: HashMap, pub selectable: bool, pub geom: PaneGeom, pub geom_override: Option, pub content_offset: Offset, /// Per-side outer gap. Set by the pane container, which knows the viewport: /// a side only gets a gap where one belongs, so two neighbours produce ONE /// gap between them rather than two. gap_offset: Offset, /// Whether this pane currently draws a frame. See set_frame(). draws_frame: bool, pub send_plugin_instructions: SenderWithContext, pub active_at: Instant, pub pane_title: String, pub pane_name: String, pub style: Style, sixel_image_store: Rc>, kitty_image_store: Rc>, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, link_handler: Rc>, character_cell_size: Rc>>, vte_parsers: HashMap, grids: HashMap, cursor_visibility: HashMap>, prev_pane_name: String, frame: HashMap, borderless: bool, exclude_from_sync: bool, pane_frame_color_override: Option<(PaletteColor, Option)>, invoked_with: Option, loading_indication: LoadingIndication, requesting_permissions: Option, debug: bool, arrow_fonts: bool, styled_underlines: bool, should_be_suppressed: bool, text_being_pasted: Option>, supports_mouse_selection: bool, } impl PluginPane { pub fn new( pid: u32, position_and_size: PaneGeom, send_plugin_instructions: SenderWithContext, title: String, pane_name: String, sixel_image_store: Rc>, kitty_image_store: Rc>, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, link_handler: Rc>, character_cell_size: Rc>>, currently_connected_clients: Vec, style: Style, invoked_with: Option, debug: bool, arrow_fonts: bool, styled_underlines: bool, ) -> Self { let loading_indication = LoadingIndication::new(title.clone()).with_colors(style.colors); let initial_loading_message = loading_indication.to_string(); let mut plugin = PluginPane { pid, should_render: HashMap::new(), selectable: true, geom: position_and_size, geom_override: None, send_plugin_instructions, active_at: Instant::now(), frame: HashMap::new(), content_offset: Offset::default(), gap_offset: Offset::default(), draws_frame: true, pane_title: title, borderless: false, pane_name: pane_name.clone(), prev_pane_name: pane_name, terminal_emulator_colors, terminal_emulator_color_codes, exclude_from_sync: false, link_handler, character_cell_size, sixel_image_store, kitty_image_store, vte_parsers: HashMap::new(), grids: HashMap::new(), cursor_visibility: HashMap::new(), style, pane_frame_color_override: None, invoked_with, loading_indication, requesting_permissions: None, debug, arrow_fonts, styled_underlines, should_be_suppressed: false, text_being_pasted: None, supports_mouse_selection: false, }; for client_id in currently_connected_clients { plugin.handle_plugin_bytes(client_id, initial_loading_message.as_bytes().to_vec()); } plugin } } impl Pane for PluginPane { // FIXME: These position and size things should all be moved to default trait implementations, // with something like a get_pos_and_sz() method underpinning all of them. Alternatively and // preferably, just use an enum and not a trait object fn x(&self) -> usize { self.geom_override.unwrap_or(self.geom).x } fn y(&self) -> usize { self.geom_override.unwrap_or(self.geom).y } fn rows(&self) -> usize { self.geom_override.unwrap_or(self.geom).rows.as_usize() } fn cols(&self) -> usize { self.geom_override.unwrap_or(self.geom).cols.as_usize() } fn get_content_x(&self) -> usize { self.x() + self.content_offset.left } fn get_content_y(&self) -> usize { self.y() + self.content_offset.top } fn get_content_columns(&self) -> usize { // content columns might differ from the pane's columns if the pane has a frame // in that case they would be 2 less self.cols() .saturating_sub(self.content_offset.left + self.content_offset.right) } fn get_content_rows(&self) -> usize { // content rows might differ from the pane's rows if the pane has a frame // in that case they would be 2 less self.rows() .saturating_sub(self.content_offset.top + self.content_offset.bottom) } fn reset_size_and_position_override(&mut self) { self.geom_override = None; self.resize_grids(); self.set_should_render(true); } fn set_geom(&mut self, position_and_size: PaneGeom) { let is_pinned = self.geom.is_pinned; self.geom = position_and_size; self.geom.is_pinned = is_pinned; self.resize_grids(); self.set_should_render(true); } fn set_geom_override(&mut self, pane_geom: PaneGeom) { self.geom_override = Some(pane_geom); self.resize_grids(); self.set_should_render(true); } fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) { self.set_client_should_render(client_id, true); let mut vte_bytes = bytes; if let Some(plugin_permission) = &self.requesting_permissions { vte_bytes = self .display_request_permission_message(plugin_permission) .into(); } let grid = get_or_create_grid!(self, client_id); // this is part of the plugin contract, whenever we update the plugin and call its render function, we delete the existing viewport // and scroll, reset the cursor position and make sure all the viewport is rendered grid.delete_viewport_and_scroll(); grid.reset_cursor_position(); grid.render_full_viewport(); let vte_parser = self .vte_parsers .entry(client_id) .or_insert_with(|| vte::Parser::new()); vte_parser.advance(grid, &vte_bytes); self.should_render.insert(client_id, true); } fn cursor_coordinates(&self, client_id: Option) -> Option<(usize, usize, bool)> { let own_content_columns = self.get_content_columns(); let own_content_rows = self.get_content_rows(); let Offset { top, left, .. } = self.content_offset; if let Some(coordinates) = client_id.and_then(|client_id| self.cursor_visibility.get(&client_id)) { coordinates .map(|(x, y)| (x + left, y + top)) .and_then(|(x, y)| { if x >= own_content_columns || y >= own_content_rows { None } else { Some((x, y, true)) // plugins always show cursor when position is set } }) } else { None } } fn adjust_input_to_terminal( &mut self, key_with_modifier: &Option, mut raw_input_bytes: Vec, _raw_input_bytes_are_kitty: bool, client_id: Option, ) -> Option { if client_id .and_then(|c| self.grids.get(&c)) .map(|g| g.has_selection()) .unwrap_or(false) { self.reset_selection(client_id); None } else if let Some(requesting_permissions) = &self.requesting_permissions { let permissions = requesting_permissions.permissions.clone(); if let Some(key_with_modifier) = key_with_modifier { match key_with_modifier.bare_key { BareKey::Char('y') if key_with_modifier.has_no_modifiers() => { Some(AdjustedInput::PermissionRequestResult( permissions, PermissionStatus::Granted, )) }, BareKey::Char('n') if key_with_modifier.has_no_modifiers() => { Some(AdjustedInput::PermissionRequestResult( permissions, PermissionStatus::Denied, )) }, _ => None, } } else { match raw_input_bytes.as_slice() { // Y or y &[89] | &[121] => Some(AdjustedInput::PermissionRequestResult( permissions, PermissionStatus::Granted, )), // N or n &[78] | &[110] => Some(AdjustedInput::PermissionRequestResult( permissions, PermissionStatus::Denied, )), _ => None, } } } else if let Some(key_with_modifier) = key_with_modifier { Some(AdjustedInput::WriteKeyToPlugin(key_with_modifier.clone())) } else if raw_input_bytes.as_slice() == BRACKETED_PASTE_BEGIN { self.text_being_pasted = Some(vec![]); None } else if raw_input_bytes.as_slice() == BRACKETED_PASTE_END { if let Some(text_being_pasted) = self.text_being_pasted.take() { match String::from_utf8(text_being_pasted) { Ok(pasted_text) => { let _ = self .send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), client_id, Event::PastedText(pasted_text), )])); }, Err(e) => { log::error!("Failed to convert pasted bytes as utf8 {:?}", e); }, } } None } else if let Some(pasted_text) = self.text_being_pasted.as_mut() { pasted_text.append(&mut raw_input_bytes); None } else { Some(AdjustedInput::WriteBytesToTerminal(raw_input_bytes)) } } fn position_and_size(&self) -> PaneGeom { self.geom } fn current_geom(&self) -> PaneGeom { self.geom_override.unwrap_or(self.geom) } fn geom_override(&self) -> Option { self.geom_override } fn should_render(&self) -> bool { // set should_render for all clients self.should_render.values().any(|v| *v) } fn set_should_render(&mut self, should_render: bool) { self.should_render .values_mut() .for_each(|v| *v = should_render); } fn refresh_for_focus_change(&mut self) { if self.style.inactive_pane_grayscale > 0 { // BOTH calls are required, and it is not obvious why: // render_full_viewport() marks the grid's output BUFFER so the // whole viewport is emitted rather than just a diff, but // render() early-returns unless should_render is set. // Without the second call an IDLE pane never re-renders, so it keeps // its previous colours indefinitely. Shell panes hid this because // they set should_render constantly on their own; a pane sitting in // neovim with no output did not, and stayed in colour. self.set_should_render(true); self.render_full_viewport(); } } fn render_full_viewport(&mut self) { // this marks the pane for a full re-render, rather than just rendering the // diff as it usually does with the OutputBuffer self.frame.clear(); for grid in self.grids.values_mut() { grid.render_full_viewport(); } } fn selectable(&self) -> bool { self.selectable } fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } fn show_cursor(&mut self, client_id: ClientId, cursor_position: Option<(usize, usize)>) { self.cursor_visibility.insert(client_id, cursor_position); self.should_render.insert(client_id, true); } fn request_permissions_from_user(&mut self, permissions: Option) { self.requesting_permissions = permissions; self.handle_plugin_bytes_for_all_clients(Default::default()); // to trigger the render of // the permission message } fn render( &mut self, client_id: Option, ) -> Result< Option<( Vec, Option, Vec, Vec, )>, > { if client_id.is_none() { return Ok(None); } if let Some(client_id) = client_id { if self.should_render.get(&client_id).copied().unwrap_or(false) { let content_x = self.get_content_x(); let content_y = self.get_content_y(); let rows = self.get_content_rows(); let columns = self.get_content_columns(); if rows < 1 || columns < 1 { return Ok(None); } if let Some(grid) = self.grids.get_mut(&client_id) { match grid.render(content_x, content_y, &self.style) { Ok(rendered_assets) => { self.should_render.insert(client_id, false); return Ok(rendered_assets); }, e => return e, } } } } Ok(None) } fn render_frame( &mut self, client_id: ClientId, frame_params: FrameParams, input_mode: InputMode, ) -> Result, Option)>> { if self.borderless { return Ok(None); } let frame_geom = frame_params .frame_geom_override .unwrap_or_else(|| self.current_geom()); // computed before the `grid` mutable borrow below let content_rect = Viewport { x: self.get_content_x(), y: self.get_content_y(), cols: self.get_content_columns(), rows: self.get_content_rows(), }; let grid = get_or_create_grid!(self, client_id); let err_context = || format!("failed to render frame for client {client_id}"); let pane_title = if frame_params.blank_title { String::new() } else if let Some(text_color_override) = self .pane_frame_color_override .as_ref() .and_then(|(_color, text)| text.as_ref()) { text_color_override.into() } else if self.pane_name.is_empty() && input_mode == InputMode::RenamePane && frame_params.is_main_client { String::from("Enter name...") } else if self.pane_name.is_empty() { grid.title .clone() .unwrap_or_else(|| self.pane_title.clone()) } else { self.pane_name.clone() }; let is_pinned = frame_geom.is_pinned; // see the matching comment in terminal_pane.rs: the frame is drawn // inset from the pane's allocated geometry to produce the outer gap let outer_frame_viewport = Viewport::from(frame_geom); let frame_viewport = outer_frame_viewport.inset_by(if self.draws_frame { self.gap_offset } else { Offset::default() }); // Every cell of the pane's allocation must be written by exactly one // pane each render, or stale glyphs survive as ghosting. The gap and // padding insets carve out TWO regions that neither the frame nor the // content covers: // 1. allocation \ drawn frame (the outer gap) // 2. frame inner \ content rect (the inner padding) // Both are blanked explicitly below. let frame_interior = if frame_params.should_draw_pane_frames { Viewport { x: frame_viewport.x.saturating_add(1), y: frame_viewport.y.saturating_add(1), cols: frame_viewport.cols.saturating_sub(2), rows: frame_viewport.rows.saturating_sub(2), } } else { // Frames off: the cells around the content are NOT ours. They hold // the inter-pane boundaries drawn by ui/boundaries.rs, and // content_offset deliberately leaves room for them. Collapsing the // interior onto the content rect emits no padding ring at all, // rather than erasing those boundaries. content_rect }; let mut frame = PaneFrame::new( frame_viewport, grid.scrollback_position_and_length(), pane_title, frame_params, ) .is_pinned(is_pinned); if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() { frame.override_color(*frame_color_override); } let res = match self.frame.get(&client_id) { // TODO: use and_then or something? Some(last_frame) => { if &frame != last_frame || is_pinned { if !self.borderless { let mut frame_output = frame.render().with_context(err_context)?; frame_output .0 .extend(gap_ring_chunks(outer_frame_viewport, frame_viewport) .into_iter() .chain(gap_ring_chunks(frame_interior, content_rect))); self.frame.insert(client_id, frame); Some(frame_output) } else { None } } else { None } }, None => { if !self.borderless { let mut frame_output = frame.render().with_context(err_context)?; frame_output .0 .extend(gap_ring_chunks(outer_frame_viewport, frame_viewport) .into_iter() .chain(gap_ring_chunks(frame_interior, content_rect))); self.frame.insert(client_id, frame); Some(frame_output) } else { None } }, }; Ok(res) } fn render_fake_cursor( &mut self, _cursor_color: PaletteColor, _text_color: PaletteColor, ) -> Option { None } fn render_terminal_title(&mut self, input_mode: InputMode) -> String { let pane_title = if self.pane_name.is_empty() && input_mode == InputMode::RenamePane { "Enter name..." } else if self.pane_name.is_empty() { &self.pane_title } else { &self.pane_name }; make_terminal_title(pane_title) } fn update_name(&mut self, name: &str) { match name { "\0" => { self.pane_name = String::new(); }, "\u{007F}" | "\u{0008}" => { //delete and backspace keys self.pane_name.pop(); }, c => { self.pane_name.push_str(c); }, } } fn pid(&self) -> PaneId { PaneId::Plugin(self.pid) } fn reduce_height(&mut self, percent: f64) { if let Some(p) = self.geom.rows.as_percent() { self.geom.rows.set_percent(p - percent); self.resize_grids(); self.set_should_render(true); } } fn increase_height(&mut self, percent: f64) { if let Some(p) = self.geom.rows.as_percent() { self.geom.rows.set_percent(p + percent); self.resize_grids(); self.set_should_render(true); } } fn reduce_width(&mut self, percent: f64) { if let Some(p) = self.geom.cols.as_percent() { self.geom.cols.set_percent(p - percent); self.resize_grids(); self.set_should_render(true); } } fn increase_width(&mut self, percent: f64) { if let Some(p) = self.geom.cols.as_percent() { self.geom.cols.set_percent(p + percent); self.resize_grids(); self.set_should_render(true); } } fn push_down(&mut self, count: usize) { self.geom.y += count; self.resize_grids(); self.set_should_render(true); } fn push_right(&mut self, count: usize) { self.geom.x += count; self.resize_grids(); self.set_should_render(true); } fn pull_left(&mut self, count: usize) { self.geom.x -= count; self.resize_grids(); self.set_should_render(true); } fn pull_up(&mut self, count: usize) { self.geom.y -= count; self.resize_grids(); self.set_should_render(true); } fn dump_screen(&self, full: bool, client_id: Option) -> String { client_id .and_then(|c| self.grids.get(&c)) .map(|g| g.dump_screen(full)) .unwrap_or_else(|| "".to_owned()) } fn scroll_up(&mut self, count: usize, client_id: ClientId) { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::ScrollUp(count)), )])) .unwrap(); } fn scroll_down(&mut self, count: usize, client_id: ClientId) { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::ScrollDown(count)), )])) .unwrap(); } fn scroll_left(&mut self, count: usize, client_id: ClientId) { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::ScrollLeft(count)), )])) .unwrap(); } fn scroll_right(&mut self, count: usize, client_id: ClientId) { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::ScrollRight(count)), )])) .unwrap(); } fn clear_screen(&mut self) { // do nothing } fn clear_scroll(&mut self) { // noop } fn start_selection(&mut self, start: &Position, client_id: ClientId) { if self.supports_mouse_selection { if let Some(grid) = self.grids.get_mut(&client_id) { grid.start_selection(start); self.set_should_render(true); } } else { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::LeftClick(start.line.0, start.column.0)), )])) .unwrap(); } } fn update_selection(&mut self, position: &Position, client_id: ClientId) { if self.supports_mouse_selection { if let Some(grid) = self.grids.get_mut(&client_id) { grid.update_selection(position); self.set_should_render(true); // TODO: no?? } } else { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::Hold(position.line.0, position.column.0)), )])) .unwrap(); } } fn end_selection(&mut self, end: &Position, client_id: ClientId) { if self.supports_mouse_selection { if let Some(grid) = self.grids.get_mut(&client_id) { grid.end_selection(end); } } else { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::Release(end.line(), end.column())), )])) .unwrap(); } } fn reset_selection(&mut self, client_id: Option) { if let Some(grid) = client_id.and_then(|c| self.grids.get_mut(&c)) { grid.reset_selection(); self.set_should_render(true); } } fn supports_mouse_selection(&self) -> bool { self.supports_mouse_selection } fn get_selected_text(&self, client_id: ClientId) -> Option { if let Some(grid) = self.grids.get(&client_id) { grid.get_selected_text() } else { None } } fn is_scrolled(&self) -> bool { false } fn active_at(&self) -> Instant { self.active_at } fn set_active_at(&mut self, time: Instant) { self.active_at = time; } fn set_frame(&mut self, frame: bool) { // Recorded, not just discarded: gap/padding must be disabled when this // pane is not drawing a frame. In frameless mode the cells around the // content belong to the inter-pane boundary renderer, and a pane that // insets its content there leaves cells that nobody repaints. self.draws_frame = frame; self.frame.clear(); } fn set_gap_offset(&mut self, gap_offset: Offset) { self.gap_offset = gap_offset; } fn set_content_offset(&mut self, offset: Offset) { let inset = if self.borderless || !self.draws_frame { // borderless panes are chrome, sized exactly to their content, so // any inset makes them vanish. Frameless panes must not be inset // either: the cells around their content are owned by the // inter-pane boundary renderer, so a moat we create here is a moat // nobody repaints -> stale glyphs on resize. Offset::default() } else { Offset { top: self.gap_offset.top.saturating_add(self.style.pane_padding.top), bottom: self.gap_offset.bottom.saturating_add(self.style.pane_padding.bottom), left: self.gap_offset.left.saturating_add(self.style.pane_padding.left), right: self.gap_offset.right.saturating_add(self.style.pane_padding.right), } }; self.content_offset = offset.expanded_by_offset_within(inset, self.cols(), self.rows()); self.resize_grids(); } fn get_content_offset(&self) -> Offset { self.content_offset } fn store_pane_name(&mut self) { if self.pane_name != self.prev_pane_name { self.prev_pane_name = self.pane_name.clone() } } fn load_pane_name(&mut self) { if self.pane_name != self.prev_pane_name { self.pane_name = self.prev_pane_name.clone() } } fn set_borderless(&mut self, should_be_borderless: bool) { self.borderless = should_be_borderless; if should_be_borderless { self.set_content_offset(Offset::default()); } else { self.set_content_offset(Offset::frame(1)); } } fn borderless(&self) -> bool { self.borderless } fn set_exclude_from_sync(&mut self, exclude_from_sync: bool) { self.exclude_from_sync = exclude_from_sync; } fn exclude_from_sync(&self) -> bool { self.exclude_from_sync } fn handle_right_click(&mut self, to: &Position, client_id: ClientId) { self.send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::RightClick(to.line.0, to.column.0)), )])) .unwrap(); } fn add_red_pane_frame_color_override(&mut self, error_text: Option) { self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text)); } fn add_highlight_pane_frame_color_override( &mut self, text: Option, _client_id: Option, ) { // TODO: if we have a client_id, we should only highlight the frame for this client self.pane_frame_color_override = Some((self.style.colors.frame_highlight.base, text)); } fn clear_pane_frame_color_override(&mut self, _client_id: Option) { // TODO: if we have a client_id, we should only clear the highlight for this client self.pane_frame_color_override = None; } fn frame_color_override(&self) -> Option { self.pane_frame_color_override .as_ref() .map(|(color, _text)| *color) } fn invoked_with(&self) -> &Option { &self.invoked_with } fn set_title(&mut self, title: String) { self.pane_title = title; } fn update_loading_indication(&mut self, loading_indication: LoadingIndication) { if self.loading_indication.ended && !loading_indication.is_error() { return; } self.loading_indication.merge(loading_indication); self.handle_plugin_bytes_for_all_clients( self.loading_indication.to_string().as_bytes().to_vec(), ); } fn start_loading_indication(&mut self, loading_indication: LoadingIndication) { self.loading_indication.merge(loading_indication); self.handle_plugin_bytes_for_all_clients( self.loading_indication.to_string().as_bytes().to_vec(), ); } fn progress_animation_offset(&mut self) { if self.loading_indication.ended { return; } self.loading_indication.progress_animation_offset(); self.handle_plugin_bytes_for_all_clients( self.loading_indication.to_string().as_bytes().to_vec(), ); } fn current_title(&self) -> String { if self.pane_name.is_empty() { self.pane_title.to_owned() } else { self.pane_name.to_owned() } } fn custom_title(&self) -> Option { if self.pane_name.is_empty() { None } else { Some(self.pane_name.clone()) } } fn rename(&mut self, buf: Vec) { self.pane_name = String::from_utf8_lossy(&buf).to_string(); self.set_should_render(true); } fn update_theme(&mut self, theme: Styling) { self.style.colors = theme.clone(); for grid in self.grids.values_mut() { grid.update_theme(theme.clone()); } } fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) { self.arrow_fonts = should_support_arrow_fonts; for grid in self.grids.values_mut() { grid.update_arrow_fonts(should_support_arrow_fonts); } self.set_should_render(true); } fn update_rounded_corners(&mut self, rounded_corners: bool) { self.style.rounded_corners = rounded_corners; self.frame.clear(); } fn update_frame_layout( &mut self, title_alignment: zellij_utils::data::PaneTitleAlignment, gap_x: usize, gap_y: usize, padding: zellij_utils::data::PanePadding, inactive_grayscale: u8, hide_default_pane_names: bool, status_bar_button_style: zellij_utils::input::theme::StatusBarButtonStyle, ) { self.style.pane_title_alignment = title_alignment; self.style.pane_gap_x = gap_x; self.style.pane_gap_y = gap_y; self.style.pane_padding = padding; self.style.inactive_pane_grayscale = inactive_grayscale; self.style.hide_default_pane_names = hide_default_pane_names; self.style.status_bar_button_style = status_bar_button_style; self.frame.clear(); } fn set_should_be_suppressed(&mut self, should_be_suppressed: bool) { self.should_be_suppressed = should_be_suppressed; } fn query_should_be_suppressed(&self) -> bool { self.should_be_suppressed } fn toggle_pinned(&mut self) { self.geom.is_pinned = !self.geom.is_pinned; } fn set_pinned(&mut self, should_be_pinned: bool) { self.geom.is_pinned = should_be_pinned; } fn intercept_left_mouse_click(&mut self, position: &Position, client_id: ClientId) -> bool { if self.position_is_on_frame(position) { let relative_position = self.relative_position(position); if let Some(client_frame) = self.frame.get_mut(&client_id) { if client_frame.clicked_on_pinned(relative_position) { self.toggle_pinned(); return true; } } } false } fn intercept_mouse_event_on_frame(&mut self, event: &MouseEvent, client_id: ClientId) -> bool { if self.position_is_on_frame(&event.position) { let relative_position = self.relative_position(&event.position); if let MouseEventType::Press = event.event_type { if let Some(client_frame) = self.frame.get_mut(&client_id) { if client_frame.clicked_on_pinned(relative_position) { self.toggle_pinned(); return true; } } } } false } fn reset_logical_position(&mut self) { self.geom.logical_position = None; } fn mouse_event(&self, event: &MouseEvent, client_id: ClientId) -> Option { match event.event_type { MouseEventType::Motion if !event.left && !event.right && !event.middle && !event.wheel_up && !event.wheel_down => { let _ = self .send_plugin_instructions .send(PluginInstruction::Update(vec![( Some(self.pid), Some(client_id), Event::Mouse(Mouse::Hover(event.position.line(), event.position.column())), )])); }, _ => {}, } None } fn set_mouse_selection_support(&mut self, selection_support: bool) { self.supports_mouse_selection = selection_support; if !selection_support { let client_ids_with_grids: Vec = self.grids.keys().copied().collect(); for client_id in client_ids_with_grids { self.reset_selection(Some(client_id)); } } } fn pane_contents( &self, client_id: Option, get_full_scrollback: bool, max_scrollback_lines: Option, ) -> PaneContents { client_id .and_then(|c| self.grids.get(&c)) .map(|g| g.pane_contents(get_full_scrollback, max_scrollback_lines)) .unwrap_or_else(Default::default) } fn pane_contents_with_ansi( &self, client_id: Option, get_full_scrollback: bool, max_scrollback_lines: Option, ) -> PaneContents { client_id .and_then(|c| self.grids.get(&c)) .map(|g| g.pane_contents_with_ansi(get_full_scrollback, max_scrollback_lines)) .unwrap_or_else(Default::default) } } impl PluginPane { fn resize_grids(&mut self) { let content_rows = self.get_content_rows(); let content_columns = self.get_content_columns(); for grid in self.grids.values_mut() { grid.change_size(content_rows, content_columns); } self.set_should_render(true); } fn set_client_should_render(&mut self, client_id: ClientId, should_render: bool) { self.should_render.insert(client_id, should_render); } fn handle_plugin_bytes_for_all_clients(&mut self, bytes: VteBytes) { let client_ids: Vec = self.grids.keys().copied().collect(); for client_id in client_ids { self.handle_plugin_bytes(client_id, bytes.clone()); } } fn display_request_permission_message(&self, plugin_permission: &PluginPermission) -> String { let bold_white = style!(self.style.colors.text_unselected.base).bold(); let cyan = style!(self.style.colors.text_unselected.emphasis_1).bold(); let orange = style!(self.style.colors.text_unselected.emphasis_0).bold(); let green = style!(self.style.colors.text_unselected.emphasis_2).bold(); let mut messages = String::new(); let permissions: BTreeSet = plugin_permission.permissions.clone().into_iter().collect(); let min_row_count = permissions.len() + 4; if self.rows() >= min_row_count { messages.push_str(&format!( "{} {} {}\n", bold_white.paint("Plugin"), cyan.paint(&plugin_permission.name), bold_white.paint("asks permission to:"), )); permissions.iter().enumerate().for_each(|(i, p)| { messages.push_str(&format!( "\n\r{}. {}", bold_white.paint(&format!("{}", i + 1)), orange.paint(p.display_name()) )); }); messages.push_str(&format!( "\n\n\r{} {}", bold_white.paint("Allow?"), green.paint("(y/n)"), )); } else { messages.push_str(&format!( "{} {}. {} {}", bold_white.paint("This plugin asks permission to:"), orange.paint( permissions .iter() .map(|p| p.to_string()) .collect::>() .join(", ") ), bold_white.paint("Allow?"), green.paint("(y/n)"), )); } messages } }