diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 1a26ca14f2..27dc3262d9 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -24,7 +24,7 @@ struct State { tabs: Vec, tip_name: String, mode_info: ModeInfo, - diplay_text_copied_hint: bool, + text_copy_destination: Option, display_system_clipboard_failure: bool, } @@ -156,14 +156,14 @@ impl ZellijPlugin for State { Event::TabUpdate(tabs) => { self.tabs = tabs; } - Event::CopyToClipboard => { - self.diplay_text_copied_hint = true; + Event::CopyToClipboard(copy_destination) => { + self.text_copy_destination = Some(copy_destination); } Event::SystemClipboardFailure => { self.display_system_clipboard_failure = true; } Event::InputReceived => { - self.diplay_text_copied_hint = false; + self.text_copy_destination = None; self.display_system_clipboard_failure = false; } _ => {} @@ -186,64 +186,7 @@ impl ZellijPlugin for State { ); let first_line = format!("{}{}", superkey, ctrl_keys); - - let mut second_line = LinePart::default(); - for t in &mut self.tabs { - if t.active { - match self.mode_info.mode { - InputMode::Normal => { - if t.is_fullscreen_active { - second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else if self.display_system_clipboard_failure { - system_clipboard_error(&self.mode_info.palette) - } else { - fullscreen_panes_to_hide(&self.mode_info.palette, t.panes_to_hide) - } - } else { - second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else if self.display_system_clipboard_failure { - system_clipboard_error(&self.mode_info.palette) - } else { - keybinds(&self.mode_info, &self.tip_name, cols) - } - } - } - InputMode::Locked => { - if t.is_fullscreen_active { - second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else if self.display_system_clipboard_failure { - system_clipboard_error(&self.mode_info.palette) - } else { - locked_fullscreen_panes_to_hide( - &self.mode_info.palette, - t.panes_to_hide, - ) - } - } else { - second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else if self.display_system_clipboard_failure { - system_clipboard_error(&self.mode_info.palette) - } else { - keybinds(&self.mode_info, &self.tip_name, cols) - } - } - } - _ => { - second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else if self.display_system_clipboard_failure { - system_clipboard_error(&self.mode_info.palette) - } else { - keybinds(&self.mode_info, &self.tip_name, cols) - } - } - } - } - } + let second_line = self.second_line(cols); // [48;5;238m is gray background, [0K is so that it fills the rest of the line // [m is background reset, [0K is so that it clears the rest of the line @@ -258,3 +201,32 @@ impl ZellijPlugin for State { println!("\u{1b}[m{}\u{1b}[0K", second_line); } } + +impl State { + fn second_line(&self, cols: usize) -> LinePart { + 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.palette, copy_destination) + } else if self.display_system_clipboard_failure { + system_clipboard_error(&self.mode_info.palette) + } else if let Some(active_tab) = active_tab { + if active_tab.is_fullscreen_active { + match self.mode_info.mode { + InputMode::Normal => { + fullscreen_panes_to_hide(&self.mode_info.palette, active_tab.panes_to_hide) + } + InputMode::Locked => locked_fullscreen_panes_to_hide( + &self.mode_info.palette, + active_tab.panes_to_hide, + ), + _ => keybinds(&self.mode_info, &self.tip_name, cols), + } + } else { + keybinds(&self.mode_info, &self.tip_name, cols) + } + } else { + LinePart::default() + } + } +} diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 32a444255c..13e847b998 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -229,12 +229,19 @@ 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: &Palette) -> LinePart { - let hint = " Text copied to clipboard"; +pub fn text_copied_hint(palette: &Palette, copy_destination: CopyDestination) -> LinePart { let green_color = match palette.green { PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), PaletteColor::EightBit(color) => Fixed(color), }; + let hint = match copy_destination { + CopyDestination::Command => "Text piped to external command", + #[cfg(not(target_os = "macos"))] + CopyDestination::Primary => "Text copied to primary selection", + #[cfg(target_os = "macos")] // primary selection does not exist on macos + CopyDestination::Primary => "Text copied to clipboard", + CopyDestination::System => "Text copied to clipboard", + }; LinePart { part: Style::new().fg(green_color).bold().paint(hint).to_string(), len: hint.len(), diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c8f80eba4b..6039c6f63e 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -6,6 +6,7 @@ use std::os::unix::io::RawFd; use std::rc::Rc; use std::str; +use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::Size; use zellij_utils::{input::layout::Layout, position::Position, zellij_tile}; @@ -194,10 +195,12 @@ pub(crate) struct Screen { draw_pane_frames: bool, session_is_mirrored: bool, copy_command: Option, + copy_clipboard: Clipboard, } impl Screen { /// Creates and returns a new [`Screen`]. + #[allow(clippy::too_many_arguments)] pub fn new( bus: Bus, client_attributes: &ClientAttributes, @@ -206,6 +209,7 @@ impl Screen { draw_pane_frames: bool, session_is_mirrored: bool, copy_command: Option, + copy_clipboard: Clipboard, ) -> Self { Screen { bus, @@ -222,6 +226,7 @@ impl Screen { draw_pane_frames, session_is_mirrored, copy_command, + copy_clipboard, } } @@ -495,6 +500,7 @@ impl Screen { self.session_is_mirrored, client_id, self.copy_command.clone(), + self.copy_clipboard.clone(), ); tab.apply_layout(layout, new_pids, tab_index, client_id); if self.session_is_mirrored { @@ -700,6 +706,7 @@ pub(crate) fn screen_thread_main( draw_pane_frames, session_is_mirrored, config_options.copy_command, + config_options.copy_clipboard.unwrap_or_default(), ); loop { let (event, mut err_ctx) = screen diff --git a/zellij-server/src/tab/clipboard.rs b/zellij-server/src/tab/clipboard.rs new file mode 100644 index 0000000000..0529bb0020 --- /dev/null +++ b/zellij-server/src/tab/clipboard.rs @@ -0,0 +1,50 @@ +use zellij_tile::prelude::CopyDestination; +use zellij_utils::{anyhow::Result, input::options::Clipboard}; + +use crate::ClientId; + +use super::{copy_command::CopyCommand, Output}; + +pub(crate) enum ClipboardProvider { + Command(CopyCommand), + Osc52(Clipboard), +} + +impl ClipboardProvider { + pub(crate) fn set_content( + &self, + content: &str, + output: &mut Output, + client_ids: impl Iterator, + ) -> Result<()> { + match &self { + ClipboardProvider::Command(command) => { + command.set(content.to_string())?; + } + ClipboardProvider::Osc52(clipboard) => { + let dest = match clipboard { + #[cfg(not(target_os = "macos"))] + Clipboard::Primary => 'p', + #[cfg(target_os = "macos")] // primary selection does not exist on macos + Clipboard::Primary => 'c', + Clipboard::System => 'c', + }; + output.push_str_to_multiple_clients( + &format!("\u{1b}]52;{};{}\u{1b}\\", dest, base64::encode(content)), + client_ids, + ); + } + }; + Ok(()) + } + + pub(crate) fn as_copy_destination(&self) -> CopyDestination { + match self { + ClipboardProvider::Command(_) => CopyDestination::Command, + ClipboardProvider::Osc52(clipboard) => match clipboard { + Clipboard::Primary => CopyDestination::Primary, + Clipboard::System => CopyDestination::System, + }, + } + } +} diff --git a/zellij-server/src/tab/copy_command.rs b/zellij-server/src/tab/copy_command.rs index 6124095027..0b2d84cf41 100644 --- a/zellij-server/src/tab/copy_command.rs +++ b/zellij-server/src/tab/copy_command.rs @@ -1,6 +1,8 @@ use std::io::prelude::*; use std::process::{Command, Stdio}; +use zellij_utils::anyhow::{Context, Result}; + pub struct CopyCommand { command: String, args: Vec, @@ -15,25 +17,18 @@ impl CopyCommand { args: command_with_args.collect(), } } - pub fn set(&self, value: String) -> bool { - let process = match Command::new(self.command.clone()) + pub fn set(&self, value: String) -> Result<()> { + let process = Command::new(self.command.clone()) .args(self.args.clone()) .stdin(Stdio::piped()) .spawn() - { - Err(why) => { - eprintln!("couldn't spawn {}: {}", self.command, why); - return false; - } - Ok(process) => process, - }; + .with_context(|| format!("couldn't spawn {}", self.command))?; + process + .stdin + .context("could not get stdin")? + .write_all(value.as_bytes()) + .with_context(|| format!("couldn't write to {} stdin", self.command))?; - match process.stdin.unwrap().write_all(value.as_bytes()) { - Err(why) => { - eprintln!("couldn't write to {} stdin: {}", self.command, why); - false - } - Ok(_) => true, - } + Ok(()) } } diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 66533aa3f9..c91ac87a9a 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1,11 +1,13 @@ //! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size, //! as well as how they should be resized +mod clipboard; mod copy_command; mod pane_grid; mod pane_resizer; use copy_command::CopyCommand; +use zellij_utils::input::options::Clipboard; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde, zellij_tile}; @@ -41,6 +43,8 @@ use zellij_utils::{ pane_size::{Offset, PaneGeom, Size, Viewport}, }; +use self::clipboard::ClipboardProvider; + // FIXME: This should be replaced by `RESIZE_PERCENT` at some point const MIN_TERMINAL_HEIGHT: usize = 5; const MIN_TERMINAL_WIDTH: usize = 5; @@ -121,7 +125,7 @@ pub(crate) struct Tab { session_is_mirrored: bool, pending_vte_events: HashMap>, selecting_with_mouse: bool, - copy_command: Option, + clipboard_provider: ClipboardProvider, // TODO: used only to focus the pane when the layout is loaded // it seems that optimization is possible using `active_panes` focus_pane_id: Option, @@ -306,6 +310,7 @@ impl Tab { session_is_mirrored: bool, client_id: ClientId, copy_command: Option, + copy_clipboard: Clipboard, ) -> Self { let panes = BTreeMap::new(); @@ -318,6 +323,11 @@ impl Tab { let mut connected_clients = HashSet::new(); connected_clients.insert(client_id); + let clipboard_provider = match copy_command { + Some(command) => ClipboardProvider::Command(CopyCommand::new(command)), + None => ClipboardProvider::Osc52(copy_clipboard), + }; + Tab { index, position, @@ -342,7 +352,7 @@ impl Tab { connected_clients_in_app, connected_clients, selecting_with_mouse: false, - copy_command, + clipboard_provider, focus_pane_id: None, } } @@ -1936,7 +1946,7 @@ impl Tab { .send_to_plugin(PluginInstruction::Update( None, None, - Event::CopyToClipboard, + Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), )) .unwrap(); } @@ -1944,35 +1954,27 @@ impl Tab { fn write_selection_to_clipboard(&self, selection: &str) { let mut output = Output::default(); - let mut system_clipboard_failure = false; output.add_clients(&self.connected_clients); - match self.copy_command.clone() { - Some(copy_command) => { - let system_clipboard = CopyCommand::new(copy_command); - system_clipboard_failure = !system_clipboard.set(selection.to_owned()); - } - None => { - output.push_str_to_multiple_clients( - &format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)), - self.connected_clients.iter().copied(), - ); - } - } + let client_ids = self.connected_clients.iter().copied(); - // TODO: ideally we should be sending the Render instruction from the screen - self.senders - .send_to_server(ServerInstruction::Render(Some(output))) - .unwrap(); - self.senders - .send_to_plugin(PluginInstruction::Update( - None, - None, - if system_clipboard_failure { + let clipboard_event = + match self + .clipboard_provider + .set_content(selection, &mut output, client_ids) + { + Ok(_) => { + self.senders + .send_to_server(ServerInstruction::Render(Some(output))) + .unwrap(); + Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()) + } + Err(err) => { + log::error!("could not write selection to clipboard: {}", err); Event::SystemClipboardFailure - } else { - Event::CopyToClipboard - }, - )) + } + }; + self.senders + .send_to_plugin(PluginInstruction::Update(None, None, clipboard_event)) .unwrap(); } fn is_inside_viewport(&self, pane_id: &PaneId) -> bool { diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 5ac63f6b8f..527894aa48 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -9,6 +9,7 @@ use crate::{ use std::convert::TryInto; use std::path::PathBuf; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::input::options::Clipboard; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; @@ -97,6 +98,7 @@ fn create_new_tab(size: Size) -> Tab { connected_clients.insert(client_id); let connected_clients = Rc::new(RefCell::new(connected_clients)); let copy_command = None; + let copy_clipboard = Clipboard::default(); let mut tab = Tab::new( index, position, @@ -112,6 +114,7 @@ fn create_new_tab(size: Size) -> Tab { session_is_mirrored, client_id, copy_command, + copy_clipboard, ); tab.apply_layout( LayoutTemplate::default().try_into().unwrap(), diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 1fbc85fced..97a6f3516d 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -10,6 +10,7 @@ use std::convert::TryInto; use std::path::PathBuf; use zellij_utils::input::command::TerminalAction; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::input::options::Clipboard; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; @@ -92,6 +93,7 @@ fn create_new_screen(size: Size) -> Screen { let draw_pane_frames = false; let session_is_mirrored = true; let copy_command = None; + let copy_clipboard = Clipboard::default(); Screen::new( bus, &client_attributes, @@ -100,6 +102,7 @@ fn create_new_screen(size: Size) -> Screen { draw_pane_frames, session_is_mirrored, copy_command, + copy_clipboard, ) } diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index dae7cd09e8..9b9f67b288 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -76,7 +76,7 @@ pub enum Event { Key(Key), Mouse(Mouse), Timer(f64), - CopyToClipboard, + CopyToClipboard(CopyDestination), SystemClipboardFailure, InputReceived, Visible(bool), @@ -267,3 +267,10 @@ impl Default for PluginCapabilities { PluginCapabilities { arrow_fonts: true } } } + +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +pub enum CopyDestination { + Command, + Primary, + System, +} diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 749f7f8f26..607e8ea4b7 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -477,3 +477,11 @@ plugins: #copy_command: "xclip -selection clipboard" # x11 #copy_command: "wl-copy" # wayland #copy_command: "pbcopy" # osx + +# Choose the destination for copied text +# Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. +# Does not apply when using copy_command. +# Options: +# - system (default) +# - primary +# copy_clipboard: primary diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 41f241f69b..34b4dc88a4 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -79,6 +79,25 @@ pub struct Options { #[clap(long)] #[serde(default)] pub copy_command: Option, + + /// OSC52 destination clipboard + #[clap(long, arg_enum, ignore_case = true, conflicts_with = "copy-command")] + #[serde(default)] + pub copy_clipboard: Option, +} + +#[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, PartialEq)] +pub enum Clipboard { + #[serde(alias = "system")] + System, + #[serde(alias = "primary")] + Primary, +} + +impl Default for Clipboard { + fn default() -> Self { + Self::System + } } impl Options { @@ -105,6 +124,7 @@ impl Options { let on_force_close = other.on_force_close.or(self.on_force_close); let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size); let copy_command = other.copy_command.or_else(|| self.copy_command.clone()); + let copy_clipboard = other.copy_clipboard.or_else(|| self.copy_clipboard.clone()); Options { simplified_ui, @@ -118,6 +138,7 @@ impl Options { on_force_close, scroll_buffer_size, copy_command, + copy_clipboard, } } @@ -148,6 +169,7 @@ impl Options { let on_force_close = other.on_force_close.or(self.on_force_close); let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size); let copy_command = other.copy_command.or_else(|| self.copy_command.clone()); + let copy_clipboard = other.copy_clipboard.or_else(|| self.copy_clipboard.clone()); Options { simplified_ui, @@ -161,6 +183,7 @@ impl Options { on_force_close, scroll_buffer_size, copy_command, + copy_clipboard, } } @@ -210,6 +233,7 @@ impl From for Options { on_force_close: opts.on_force_close, scroll_buffer_size: opts.scroll_buffer_size, copy_command: opts.copy_command, + copy_clipboard: opts.copy_clipboard, } } } diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 7115c1b462..7eb9825ab5 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -11,6 +11,7 @@ pub mod position; pub mod setup; pub mod shared; +pub use anyhow; pub use async_std; pub use clap; pub use interprocess;