aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-02-20 17:14:43 +0100
committerGitHub <noreply@github.com>2025-02-20 17:14:43 +0100
commitf0680fdeb93dd6934cfd52e175428e0cde36fadd (patch)
tree1efb28202afe106acae03871216b8a21c06860ce
parentdocs(changelog): filepicker improvements (diff)
downloadsl0ppij-f0680fdeb93dd6934cfd52e175428e0cde36fadd.tar.gz
sl0ppij-f0680fdeb93dd6934cfd52e175428e0cde36fadd.tar.xz
sl0ppij-f0680fdeb93dd6934cfd52e175428e0cde36fadd.zip
fix(clipboard): clipboard message (#4009)
* fix clipboard message * fix clipboard message gitter * style(fmt): rustfmt
-rw-r--r--default-plugins/compact-bar/src/main.rs179
-rw-r--r--default-plugins/status-bar/src/main.rs2
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs2
-rw-r--r--default-plugins/status-bar/src/second_line.rs5
-rw-r--r--zellij-server/src/route.rs4
-rw-r--r--zellij-server/src/screen.rs14
-rw-r--r--zellij-server/src/tab/mod.rs113
-rw-r--r--zellij-utils/src/input/actions.rs10
8 files changed, 240 insertions, 89 deletions
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<LinePart>,
+ text_copy_destination: Option<CopyDestination>,
+ 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<LinePart> = 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...");
+ 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);
+ },
+ }
+ } 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<LinePart> = 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();
}
- 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);
}
- let tab = tab_style(
- tabname,
- t,
- is_alternate_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<i32>, bool, RunCommand), // Option<i32> 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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ pub fn handle_mouse_event(
+ &mut self,
+ event: &MouseEvent,
+ client_id: ClientId,
+ ) -> Result<MouseEffect> {
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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ ) -> Result<MouseEffect> {
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<bool> {
+ pub fn handle_right_click(
+ &mut self,
+ event: &MouseEvent,
+ client_id: ClientId,
+ ) -> Result<MouseEffect> {
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<bool> {
+ fn handle_middle_click(
+ &mut self,
+ event: &MouseEvent,
+ client_id: ClientId,
+ ) -> Result<MouseEffect> {
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<bool> {
+ fn handle_mouse_no_click(
+ &mut self,
+ event: &MouseEvent,
+ client_id: ClientId,
+ ) -> Result<MouseEffect> {
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<dyn Pane>> {
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
}