* initial implementation with break panes to new tab * break pane group left/right * group embed/eject panes * stack pane group on resize * close pane group * style(fmt): rustfmt * fix tests * group drag and ungroup with the mouse * fix mouse hover for multiple clients * fix for multiple clients * multiple select plugin initial * use real data in plugin * adjust functionality * fix some ux issues * reflect group mouse group selections in plugin * group/ungroup panes in Zellij * highlight frames when marked by the plugin * refactor: render function in plugin * some ui responsiveness * some more responsiveness and adjust hover text * break out functionality * stack functionality * break panes left/right and close multiple panes * fix(tab): only relayout the relevant layout when non-focused pane is closed * status bar UI * embed and float panes * work * fix some ui/ux issues * refactor: move stuff around * some responsiveness and fix search result browsing bug * change plugin pane title * differentiate group from focused pane * add keyboard shortcut * add ui to compact bar * make boundary colors appear properly without pane frames * get plugins to also display their frame color * make hover shortcuts appear on command panes * fix: do not render search string component if it's empty * BeforeClose Event and unhighlight panes on exit * some UI/UX fixes * some fixes to the catppuccin-latte theme * remove ungroup shortcut * make some ui components opaque * fix more opaque elements * fix some issues with stacking pane order * keyboard shortcuts for grouping * config to opt out of advanced mouse actions * make selected + focused frame color distinct * group marking mode * refactor: multiple-select plugin * adjust stacking group behavior * adjust flashing periods * render common modifier in group controls * add to compact bar * adjust key hint wording * add key to presets and default config * some cleanups * some refactoring * fix tests * fix plugin system tests * tests: group/ungroup/hover * test: BeforeClose plugin event * new plugin assets * style(fmt): rustfmt * remove warnings * tests: give plugin more time to load
863 lines
30 KiB
Rust
863 lines
30 KiB
Rust
use std::collections::{BTreeSet, HashMap};
|
|
use std::time::Instant;
|
|
|
|
use crate::output::{CharacterChunk, SixelImageChunk};
|
|
use crate::panes::{
|
|
grid::Grid,
|
|
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::{FrameParams, PaneFrame},
|
|
};
|
|
use crate::ClientId;
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
use vte;
|
|
use zellij_utils::data::{
|
|
BareKey, KeyWithModifier, PermissionStatus, PermissionType, PluginPermission,
|
|
};
|
|
use zellij_utils::pane_size::{Offset, SizeInPixels};
|
|
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 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.style.clone(),
|
|
$self.debug,
|
|
$self.arrow_fonts,
|
|
$self.styled_underlines,
|
|
explicitly_disable_kitty_keyboard_protocol,
|
|
);
|
|
grid.hide_cursor();
|
|
grid
|
|
})
|
|
}};
|
|
}
|
|
|
|
pub(crate) struct PluginPane {
|
|
pub pid: u32,
|
|
pub should_render: HashMap<ClientId, bool>,
|
|
pub selectable: bool,
|
|
pub geom: PaneGeom,
|
|
pub geom_override: Option<PaneGeom>,
|
|
pub content_offset: Offset,
|
|
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
|
pub active_at: Instant,
|
|
pub pane_title: String,
|
|
pub pane_name: String,
|
|
pub style: Style,
|
|
sixel_image_store: Rc<RefCell<SixelImageStore>>,
|
|
terminal_emulator_colors: Rc<RefCell<Palette>>,
|
|
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
|
|
link_handler: Rc<RefCell<LinkHandler>>,
|
|
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
|
vte_parsers: HashMap<ClientId, vte::Parser>,
|
|
grids: HashMap<ClientId, Grid>,
|
|
prev_pane_name: String,
|
|
frame: HashMap<ClientId, PaneFrame>,
|
|
borderless: bool,
|
|
exclude_from_sync: bool,
|
|
pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
|
|
invoked_with: Option<Run>,
|
|
loading_indication: LoadingIndication,
|
|
requesting_permissions: Option<PluginPermission>,
|
|
debug: bool,
|
|
arrow_fonts: bool,
|
|
styled_underlines: bool,
|
|
should_be_suppressed: bool,
|
|
text_being_pasted: Option<Vec<u8>>,
|
|
}
|
|
|
|
impl PluginPane {
|
|
pub fn new(
|
|
pid: u32,
|
|
position_and_size: PaneGeom,
|
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
|
title: String,
|
|
pane_name: String,
|
|
sixel_image_store: Rc<RefCell<SixelImageStore>>,
|
|
terminal_emulator_colors: Rc<RefCell<Palette>>,
|
|
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
|
|
link_handler: Rc<RefCell<LinkHandler>>,
|
|
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
|
currently_connected_clients: Vec<ClientId>,
|
|
style: Style,
|
|
invoked_with: Option<Run>,
|
|
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(),
|
|
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,
|
|
vte_parsers: HashMap::new(),
|
|
grids: 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,
|
|
};
|
|
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());
|
|
|
|
for &byte in &vte_bytes {
|
|
vte_parser.advance(grid, byte);
|
|
}
|
|
|
|
self.should_render.insert(client_id, true);
|
|
}
|
|
fn cursor_coordinates(&self) -> Option<(usize, usize)> {
|
|
None
|
|
}
|
|
fn adjust_input_to_terminal(
|
|
&mut self,
|
|
key_with_modifier: &Option<KeyWithModifier>,
|
|
mut raw_input_bytes: Vec<u8>,
|
|
_raw_input_bytes_are_kitty: bool,
|
|
client_id: Option<ClientId>,
|
|
) -> Option<AdjustedInput> {
|
|
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<PaneGeom> {
|
|
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 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 request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) {
|
|
self.requesting_permissions = permissions;
|
|
}
|
|
fn render(
|
|
&mut self,
|
|
client_id: Option<ClientId>,
|
|
) -> Result<Option<(Vec<CharacterChunk>, Option<String>, Vec<SixelImageChunk>)>> {
|
|
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<(Vec<CharacterChunk>, Option<String>)>> {
|
|
if self.borderless {
|
|
return Ok(None);
|
|
}
|
|
if let Some(grid) = self.grids.get(&client_id) {
|
|
let err_context = || format!("failed to render frame for client {client_id}");
|
|
let pane_title = 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 frame_geom = self.current_geom();
|
|
let is_pinned = frame_geom.is_pinned;
|
|
let mut frame = PaneFrame::new(
|
|
frame_geom.into(),
|
|
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 {
|
|
if !self.borderless {
|
|
let frame_output = frame.render().with_context(err_context)?;
|
|
self.frame.insert(client_id, frame);
|
|
Some(frame_output)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
None => {
|
|
if !self.borderless {
|
|
let frame_output = frame.render().with_context(err_context)?;
|
|
self.frame.insert(client_id, frame);
|
|
Some(frame_output)
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
};
|
|
Ok(res)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
fn render_fake_cursor(
|
|
&mut self,
|
|
_cursor_color: PaletteColor,
|
|
_text_color: PaletteColor,
|
|
) -> Option<String> {
|
|
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 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 clear_screen(&mut self) {
|
|
// do nothing
|
|
}
|
|
fn clear_scroll(&mut self) {
|
|
// noop
|
|
}
|
|
fn start_selection(&mut self, start: &Position, client_id: ClientId) {
|
|
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) {
|
|
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) {
|
|
self.send_plugin_instructions
|
|
.send(PluginInstruction::Update(vec![(
|
|
Some(self.pid),
|
|
Some(client_id),
|
|
Event::Mouse(Mouse::Release(end.line(), end.column())),
|
|
)]))
|
|
.unwrap();
|
|
}
|
|
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) {
|
|
self.frame.clear();
|
|
}
|
|
fn set_content_offset(&mut self, offset: Offset) {
|
|
self.content_offset = offset;
|
|
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, borderless: bool) {
|
|
self.borderless = borderless;
|
|
}
|
|
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<String>) {
|
|
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<String>,
|
|
_client_id: Option<ClientId>,
|
|
) {
|
|
// 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<ClientId>) {
|
|
// 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<PaletteColor> {
|
|
self.pane_frame_color_override
|
|
.as_ref()
|
|
.map(|(color, _text)| *color)
|
|
}
|
|
fn invoked_with(&self) -> &Option<Run> {
|
|
&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<String> {
|
|
if self.pane_name.is_empty() {
|
|
None
|
|
} else {
|
|
Some(self.pane_name.clone())
|
|
}
|
|
}
|
|
fn rename(&mut self, buf: Vec<u8>) {
|
|
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 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<String> {
|
|
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
|
|
}
|
|
}
|
|
|
|
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<ClientId> = 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<PermissionType> =
|
|
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::<Vec<_>>()
|
|
.join(", ")
|
|
),
|
|
bold_white.paint("Allow?"),
|
|
green.paint("(y/n)"),
|
|
));
|
|
}
|
|
|
|
messages
|
|
}
|
|
}
|