fix(clipboard): clipboard message (#4009)

* fix clipboard message

* fix clipboard message gitter

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2025-02-20 17:14:43 +01:00 committed by GitHub
parent 9edad32ee1
commit f0680fdeb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 241 additions and 90 deletions

View file

@ -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...");
}
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<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();
}
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,
}
}

View file

@ -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 {

View file

@ -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);

View file

@ -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(),
}
}

View file

@ -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,

View file

@ -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) => {

View file

@ -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>> {

View file

@ -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
}