feat(copy): allow osc52 copy destination configuration (#1022)

add copy_cliboard  option to allow configuring copy destination to primary selection instead of default clipboard
This commit is contained in:
Thomas Linford 2022-02-02 15:22:34 +01:00 committed by GitHub
parent 2799eb9160
commit 18709acde9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 189 additions and 110 deletions

View File

@ -24,7 +24,7 @@ struct State {
tabs: Vec<TabInfo>,
tip_name: String,
mode_info: ModeInfo,
diplay_text_copied_hint: bool,
text_copy_destination: Option<CopyDestination>,
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()
}
}
}

View File

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

View File

@ -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<String>,
copy_clipboard: Clipboard,
}
impl Screen {
/// Creates and returns a new [`Screen`].
#[allow(clippy::too_many_arguments)]
pub fn new(
bus: Bus<ScreenInstruction>,
client_attributes: &ClientAttributes,
@ -206,6 +209,7 @@ impl Screen {
draw_pane_frames: bool,
session_is_mirrored: bool,
copy_command: Option<String>,
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

View File

@ -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<Item = ClientId>,
) -> 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,
},
}
}
}

View File

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

View File

@ -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<RawFd, Vec<VteBytes>>,
selecting_with_mouse: bool,
copy_command: Option<String>,
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<PaneId>,
@ -306,6 +310,7 @@ impl Tab {
session_is_mirrored: bool,
client_id: ClientId,
copy_command: Option<String>,
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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,25 @@ pub struct Options {
#[clap(long)]
#[serde(default)]
pub copy_command: Option<String>,
/// OSC52 destination clipboard
#[clap(long, arg_enum, ignore_case = true, conflicts_with = "copy-command")]
#[serde(default)]
pub copy_clipboard: Option<Clipboard>,
}
#[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<CliOptions> 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,
}
}
}

View File

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