diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs index 567dc187..3775b12f 100644 --- a/default-plugins/compact-bar/src/main.rs +++ b/default-plugins/compact-bar/src/main.rs @@ -24,6 +24,8 @@ struct State { active_tab_idx: usize, mode_info: ModeInfo, tab_line: Vec, + text_copy_destination: Option, + display_system_clipboard_failure: bool, } static ARROW_SEPARATOR: &str = ""; @@ -37,6 +39,9 @@ impl ZellijPlugin for State { EventType::TabUpdate, EventType::ModeUpdate, EventType::Mouse, + EventType::CopyToClipboard, + EventType::InputReceived, + EventType::SystemClipboardFailure, ]); } @@ -77,6 +82,32 @@ impl ZellijPlugin for State { }, _ => {}, }, + Event::CopyToClipboard(copy_destination) => { + match self.text_copy_destination { + Some(text_copy_destination) => { + if text_copy_destination != copy_destination { + should_render = true; + } + }, + None => { + should_render = true; + }, + } + self.text_copy_destination = Some(copy_destination); + }, + Event::SystemClipboardFailure => { + should_render = true; + self.display_system_clipboard_failure = true; + }, + Event::InputReceived => { + if self.text_copy_destination.is_some() + || self.display_system_clipboard_failure == true + { + should_render = true; + } + self.text_copy_destination = None; + self.display_system_clipboard_failure = false; + }, _ => { eprintln!("Got unrecognized event: {:?}", event); }, @@ -85,60 +116,110 @@ impl ZellijPlugin for State { } fn render(&mut self, _rows: usize, cols: usize) { - if self.tabs.is_empty() { - return; - } - let mut all_tabs: Vec = vec![]; - let mut active_tab_index = 0; - let mut active_swap_layout_name = None; - let mut is_swap_layout_dirty = false; - let mut is_alternate_tab = false; - for t in &mut self.tabs { - let mut tabname = t.name.clone(); - if t.active && self.mode_info.mode == InputMode::RenameTab { - if tabname.is_empty() { - tabname = String::from("Enter name..."); - } - active_tab_index = t.position; - } else if t.active { - active_tab_index = t.position; - is_swap_layout_dirty = t.is_swap_layout_dirty; - active_swap_layout_name = t.active_swap_layout_name.clone(); + if let Some(copy_destination) = self.text_copy_destination { + let hint = text_copied_hint(copy_destination).part; + + let background = self.mode_info.style.colors.text_unselected.background; + match background { + PaletteColor::Rgb((r, g, b)) => { + print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", hint, r, g, b); + }, + PaletteColor::EightBit(color) => { + print!("{}\u{1b}[48;5;{}m\u{1b}[0K", hint, color); + }, } - let tab = tab_style( - tabname, - t, - is_alternate_tab, + } else if self.display_system_clipboard_failure { + let hint = system_clipboard_error().part; + let background = self.mode_info.style.colors.text_unselected.background; + match background { + PaletteColor::Rgb((r, g, b)) => { + print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", hint, r, g, b); + }, + PaletteColor::EightBit(color) => { + print!("{}\u{1b}[48;5;{}m\u{1b}[0K", hint, color); + }, + } + } else { + if self.tabs.is_empty() { + return; + } + let mut all_tabs: Vec = vec![]; + let mut active_tab_index = 0; + let mut active_swap_layout_name = None; + let mut is_swap_layout_dirty = false; + let mut is_alternate_tab = false; + for t in &mut self.tabs { + let mut tabname = t.name.clone(); + if t.active && self.mode_info.mode == InputMode::RenameTab { + if tabname.is_empty() { + tabname = String::from("Enter name..."); + } + active_tab_index = t.position; + } else if t.active { + active_tab_index = t.position; + is_swap_layout_dirty = t.is_swap_layout_dirty; + active_swap_layout_name = t.active_swap_layout_name.clone(); + } + let tab = tab_style( + tabname, + t, + is_alternate_tab, + self.mode_info.style.colors, + self.mode_info.capabilities, + ); + is_alternate_tab = !is_alternate_tab; + all_tabs.push(tab); + } + self.tab_line = tab_line( + self.mode_info.session_name.as_deref(), + all_tabs, + active_tab_index, + cols.saturating_sub(1), self.mode_info.style.colors, self.mode_info.capabilities, + self.mode_info.style.hide_session_name, + self.mode_info.mode, + &active_swap_layout_name, + is_swap_layout_dirty, ); - is_alternate_tab = !is_alternate_tab; - all_tabs.push(tab); - } - self.tab_line = tab_line( - self.mode_info.session_name.as_deref(), - all_tabs, - active_tab_index, - cols.saturating_sub(1), - self.mode_info.style.colors, - self.mode_info.capabilities, - self.mode_info.style.hide_session_name, - self.mode_info.mode, - &active_swap_layout_name, - is_swap_layout_dirty, - ); - let output = self - .tab_line - .iter() - .fold(String::new(), |output, part| output + &part.part); - let background = self.mode_info.style.colors.text_unselected.background; - match background { - PaletteColor::Rgb((r, g, b)) => { - print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b); - }, - PaletteColor::EightBit(color) => { - print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color); - }, + let output = self + .tab_line + .iter() + .fold(String::new(), |output, part| output + &part.part); + let background = self.mode_info.style.colors.text_unselected.background; + match background { + PaletteColor::Rgb((r, g, b)) => { + print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b); + }, + PaletteColor::EightBit(color) => { + print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color); + }, + } } } } + +pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart { + let hint = match copy_destination { + CopyDestination::Command => "Text piped to external command", + #[cfg(not(target_os = "macos"))] + CopyDestination::Primary => "Text copied to system primary selection", + #[cfg(target_os = "macos")] // primary selection does not exist on macos + CopyDestination::Primary => "Text copied to system clipboard", + CopyDestination::System => "Text copied to system clipboard", + }; + LinePart { + part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()), + len: hint.len(), + tab_index: None, + } +} + +pub fn system_clipboard_error() -> LinePart { + let hint = " Error using the system clipboard."; + LinePart { + part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()), + len: hint.len(), + tab_index: None, + } +} diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index db2b7c6b..65b73819 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -329,7 +329,7 @@ impl State { let active_tab = self.tabs.iter().find(|t| t.active); if let Some(copy_destination) = self.text_copy_destination { - text_copied_hint(&self.mode_info.style.colors, copy_destination) + text_copied_hint(copy_destination) } else if self.display_system_clipboard_failure { system_clipboard_error(&self.mode_info.style.colors) } else if let Some(active_tab) = active_tab { diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index b230a25b..4526e6f8 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -24,7 +24,7 @@ pub fn one_line_ui( clipboard_failure: bool, ) -> LinePart { if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination { - return text_copied_hint(&help.style.colors, text_copied_to_clipboard_destination); + return text_copied_hint(text_copied_to_clipboard_destination); } if clipboard_failure { return system_clipboard_error(&help.style.colors); diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 0c13ad14..fbc02b51 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -348,8 +348,7 @@ pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart { best_effort_shortcut_list(help, tip_body.short, max_width) } -pub fn text_copied_hint(palette: &Styling, copy_destination: CopyDestination) -> LinePart { - let green_color = palette_match!(palette.text_unselected.emphasis_2); +pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart { let hint = match copy_destination { CopyDestination::Command => "Text piped to external command", #[cfg(not(target_os = "macos"))] @@ -359,7 +358,7 @@ pub fn text_copied_hint(palette: &Styling, copy_destination: CopyDestination) -> CopyDestination::System => "Text copied to system clipboard", }; LinePart { - part: Style::new().fg(green_color).bold().paint(hint).to_string(), + part: serialize_text(&Text::new(&hint).color_range(2, ..).opaque()), len: hint.len(), } } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index ab42f825..5832cb4d 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -47,7 +47,9 @@ pub(crate) fn route_action( let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); - if !action.is_mouse_motion() { + if !action.is_mouse_action() { + // mouse actions should only send InputReceived to plugins + // if they do not result in text being marked, this is handled in Tab senders .send_to_plugin(PluginInstruction::Update(vec![( None, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index fbc1d967..0b9d0c67 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -3858,12 +3858,22 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; }, ScreenInstruction::MouseEvent(event, client_id) => { - let state_changed = screen + let mouse_effect = screen .get_active_tab_mut(client_id) .and_then(|tab| tab.handle_mouse_event(&event, client_id))?; - if state_changed { + if mouse_effect.state_changed { screen.log_and_report_session_state()?; } + if !mouse_effect.leave_clipboard_message { + let _ = screen + .bus + .senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::InputReceived, + )])); + } screen.render(None)?; }, ScreenInstruction::Copy(client_id) => { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index f92cbb9e..d3d37dca 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -147,6 +147,33 @@ enum BufferedTabInstruction { HoldPane(PaneId, Option, bool, RunCommand), // Option is the exit status, bool is is_first_run } +#[derive(Debug, Default, Copy, Clone)] +pub struct MouseEffect { + pub state_changed: bool, + pub leave_clipboard_message: bool, +} + +impl MouseEffect { + pub fn state_changed() -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: false, + } + } + pub fn leave_clipboard_message() -> Self { + MouseEffect { + state_changed: false, + leave_clipboard_message: true, + } + } + pub fn state_changed_and_leave_clipboard_message() -> Self { + MouseEffect { + state_changed: true, + leave_clipboard_message: true, + } + } +} + pub(crate) struct Tab { pub index: usize, pub position: usize, @@ -3225,7 +3252,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || { format!("failed to handle scrollwheel up at position {point:?} for client {client_id}") }; @@ -3246,7 +3273,7 @@ impl Tab { pane.scroll_up(lines, client_id); } } - Ok(false) + Ok(MouseEffect::default()) } pub fn handle_scrollwheel_down( @@ -3254,7 +3281,7 @@ impl Tab { point: &Position, lines: usize, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || { format!( "failed to handle scrollwheel down at position {point:?} for client {client_id}" @@ -3283,7 +3310,7 @@ impl Tab { } } } - Ok(false) + Ok(MouseEffect::default()) } fn get_pane_at( @@ -3353,7 +3380,11 @@ impl Tab { // returns true if the mouse event caused some sort of tab/pane state change that needs to be // reported to plugins - pub fn handle_mouse_event(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + pub fn handle_mouse_event( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { let err_context = || format!("failed to handle mouse event {event:?} for client {client_id}"); @@ -3423,7 +3454,7 @@ impl Tab { &mut self, event: &MouseEvent, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || format!("failed to handle mouse event {event:?} for client {client_id}"); let floating_panes_are_visible = self.floating_panes.panes_are_visible(); @@ -3436,7 +3467,7 @@ impl Tab { let intercepted = pane_at_position.intercept_mouse_event_on_frame(&event, client_id); if intercepted { self.set_force_render(); - return Ok(true); + return Ok(MouseEffect::state_changed()); } else if floating_panes_are_visible { // start moving if floating pane let search_selectable = false; @@ -3446,7 +3477,7 @@ impl Tab { { self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); - return Ok(true); + return Ok(MouseEffect::state_changed()); } } } else { @@ -3466,19 +3497,26 @@ impl Tab { } } else { // start selection for copy/paste + let mut leave_clipboard_message = false; pane_at_position.start_selection(&relative_position, client_id); + if pane_at_position.get_selected_text().is_some() { + leave_clipboard_message = true; + } if let PaneId::Terminal(_) = pane_at_position.pid() { self.selecting_with_mouse_in_pane = Some(pane_at_position.pid()); } + if leave_clipboard_message { + return Ok(MouseEffect::leave_clipboard_message()); + } } } - Ok(false) + Ok(MouseEffect::default()) } fn handle_inactive_pane_left_mouse_press( &mut self, event: &MouseEvent, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || format!("failed to handle mouse event {event:?} for client {client_id}"); if !self.floating_panes.panes_are_visible() { @@ -3492,7 +3530,7 @@ impl Tab { // focus it self.show_floating_panes(); self.floating_panes.focus_pane(pane_id, client_id); - return Ok(true); + return Ok(MouseEffect::state_changed()); } } let active_pane_id_before_click = self @@ -3514,25 +3552,30 @@ impl Tab { // we do this because this might be the beginning of the user dragging a pane // that was not focused // TODO: rename move_pane_with_mouse to "start_moving_pane_with_mouse"? - return Ok(self + let moved_pane_with_mouse = self .floating_panes - .move_pane_with_mouse(event.position, search_selectable)); + .move_pane_with_mouse(event.position, search_selectable); + if moved_pane_with_mouse { + return Ok(MouseEffect::state_changed()); + } else { + return Ok(MouseEffect::default()); + } } let active_pane_id_after_click = self .get_active_pane_id(client_id) .ok_or_else(|| anyhow!("Failed to find pane at position"))?; if active_pane_id_before_click != active_pane_id_after_click { // focus changed, need to report it - Ok(true) + Ok(MouseEffect::state_changed()) } else { - Ok(false) + Ok(MouseEffect::default()) } } fn handle_left_mouse_motion( &mut self, event: &MouseEvent, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || format!("failed to handle mouse event {event:?} for client {client_id}"); let pane_is_being_moved_with_mouse = self.floating_panes.pane_is_being_moved_with_mouse(); @@ -3547,7 +3590,7 @@ impl Tab { { self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); - return Ok(true); + return Ok(MouseEffect::state_changed()); } } else if let Some(pane_id_with_selection) = self.selecting_with_mouse_in_pane { if let Some(pane_with_selection) = self.get_pane_with_id_mut(pane_id_with_selection) { @@ -3563,15 +3606,16 @@ impl Tab { self.write_mouse_event_to_active_pane(event, client_id)?; } } - Ok(false) + Ok(MouseEffect::default()) } fn handle_left_mouse_release( &mut self, event: &MouseEvent, client_id: ClientId, - ) -> Result { + ) -> Result { let err_context = || format!("failed to handle mouse event {event:?} for client {client_id}"); + let mut leave_clipboard_message = false; let floating_panes_are_visible = self.floating_panes.panes_are_visible(); let copy_on_release = self.copy_on_select; @@ -3606,6 +3650,7 @@ impl Tab { let selected_text = pane_with_selection.get_selected_text(); if let Some(selected_text) = selected_text { + leave_clipboard_message = true; self.write_selection_to_clipboard(&selected_text) .with_context(err_context)?; } @@ -3621,10 +3666,18 @@ impl Tab { } else { self.write_mouse_event_to_active_pane(event, client_id)?; } - Ok(false) + if leave_clipboard_message { + Ok(MouseEffect::leave_clipboard_message()) + } else { + Ok(MouseEffect::default()) + } } - pub fn handle_right_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + pub fn handle_right_click( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { let err_context = || format!("failed to handle mouse right click for client {client_id}"); let absolute_position = event.position; @@ -3655,10 +3708,14 @@ impl Tab { } } }; - Ok(false) + Ok(MouseEffect::default()) } - fn handle_middle_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + fn handle_middle_click( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { let err_context = || format!("failed to handle mouse middle click for client {client_id}"); let absolute_position = event.position; @@ -3687,10 +3744,14 @@ impl Tab { } } }; - Ok(false) + Ok(MouseEffect::default()) } - fn handle_mouse_no_click(&mut self, event: &MouseEvent, client_id: ClientId) -> Result { + fn handle_mouse_no_click( + &mut self, + event: &MouseEvent, + client_id: ClientId, + ) -> Result { let err_context = || format!("failed to handle mouse no click for client {client_id}"); let absolute_position = event.position; @@ -3719,7 +3780,7 @@ impl Tab { } } }; - Ok(false) + Ok(MouseEffect::leave_clipboard_message()) } fn unselectable_pane_at_position(&mut self, point: &Position) -> Option<&mut Box> { diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index cd987167..f5d16174 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -10,7 +10,7 @@ use crate::data::{Direction, KeyWithModifier, PaneId, Resize}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; -use crate::input::mouse::{MouseEvent, MouseEventType}; +use crate::input::mouse::MouseEvent; use crate::input::options::OnForceClose; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; @@ -800,11 +800,9 @@ impl Action { _ => false, } } - pub fn is_mouse_motion(&self) -> bool { - if let Action::MouseEvent(mouse_event) = self { - if let MouseEventType::Motion = mouse_event.event_type { - return true; - } + pub fn is_mouse_action(&self) -> bool { + if let Action::MouseEvent(_mouse_event) = self { + return true; } false }