Add a copy command option (#996)

Usage: zellij options --copy-command "xclip -sel clip"

Co-authored-by: Christophe Verbinnen <christophev@knowbe4.com>
This commit is contained in:
Christophe Verbinnen 2022-01-15 06:38:45 -05:00 committed by GitHub
parent 6af419528f
commit 9cc2645db0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 6 deletions

View file

@ -10,7 +10,8 @@ use zellij_tile_utils::style;
use first_line::{ctrl_keys, superkey}; use first_line::{ctrl_keys, superkey};
use second_line::{ use second_line::{
fullscreen_panes_to_hide, keybinds, locked_fullscreen_panes_to_hide, text_copied_hint, fullscreen_panes_to_hide, keybinds, locked_fullscreen_panes_to_hide, system_clipboard_error,
text_copied_hint,
}; };
use tip::utils::get_cached_tip_name; use tip::utils::get_cached_tip_name;
@ -24,6 +25,7 @@ struct State {
tip_name: String, tip_name: String,
mode_info: ModeInfo, mode_info: ModeInfo,
diplay_text_copied_hint: bool, diplay_text_copied_hint: bool,
display_system_clipboard_failure: bool,
} }
register_plugin!(State); register_plugin!(State);
@ -142,6 +144,7 @@ impl ZellijPlugin for State {
EventType::TabUpdate, EventType::TabUpdate,
EventType::CopyToClipboard, EventType::CopyToClipboard,
EventType::InputReceived, EventType::InputReceived,
EventType::SystemClipboardFailure,
]); ]);
} }
@ -156,8 +159,12 @@ impl ZellijPlugin for State {
Event::CopyToClipboard => { Event::CopyToClipboard => {
self.diplay_text_copied_hint = true; self.diplay_text_copied_hint = true;
} }
Event::SystemClipboardFailure => {
self.display_system_clipboard_failure = true;
}
Event::InputReceived => { Event::InputReceived => {
self.diplay_text_copied_hint = false; self.diplay_text_copied_hint = false;
self.display_system_clipboard_failure = false;
} }
_ => {} _ => {}
} }
@ -188,12 +195,16 @@ impl ZellijPlugin for State {
if t.is_fullscreen_active { if t.is_fullscreen_active {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.palette)
} else { } else {
fullscreen_panes_to_hide(&self.mode_info.palette, t.panes_to_hide) fullscreen_panes_to_hide(&self.mode_info.palette, t.panes_to_hide)
} }
} else { } else {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, &self.tip_name, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }
@ -203,6 +214,8 @@ impl ZellijPlugin for State {
if t.is_fullscreen_active { if t.is_fullscreen_active {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.palette)
} else { } else {
locked_fullscreen_panes_to_hide( locked_fullscreen_panes_to_hide(
&self.mode_info.palette, &self.mode_info.palette,
@ -212,6 +225,8 @@ impl ZellijPlugin for State {
} else { } else {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, &self.tip_name, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }
@ -220,6 +235,8 @@ impl ZellijPlugin for State {
_ => { _ => {
second_line = if self.diplay_text_copied_hint { second_line = if self.diplay_text_copied_hint {
text_copied_hint(&self.mode_info.palette) text_copied_hint(&self.mode_info.palette)
} else if self.display_system_clipboard_failure {
system_clipboard_error(&self.mode_info.palette)
} else { } else {
keybinds(&self.mode_info, &self.tip_name, cols) keybinds(&self.mode_info, &self.tip_name, cols)
} }

View file

@ -241,6 +241,18 @@ pub fn text_copied_hint(palette: &Palette) -> LinePart {
} }
} }
pub fn system_clipboard_error(palette: &Palette) -> LinePart {
let hint = " Error using the system clipboard.";
let red_color = match palette.red {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),
PaletteColor::EightBit(color) => Fixed(color),
};
LinePart {
part: Style::new().fg(red_color).bold().paint(hint).to_string(),
len: hint.len(),
}
}
pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart {
let white_color = match palette.white { let white_color = match palette.white {
PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), PaletteColor::Rgb((r, g, b)) => RGB(r, g, b),

View file

@ -193,6 +193,7 @@ pub(crate) struct Screen {
colors: Palette, colors: Palette,
draw_pane_frames: bool, draw_pane_frames: bool,
session_is_mirrored: bool, session_is_mirrored: bool,
copy_command: Option<String>,
} }
impl Screen { impl Screen {
@ -204,6 +205,7 @@ impl Screen {
mode_info: ModeInfo, mode_info: ModeInfo,
draw_pane_frames: bool, draw_pane_frames: bool,
session_is_mirrored: bool, session_is_mirrored: bool,
copy_command: Option<String>,
) -> Self { ) -> Self {
Screen { Screen {
bus, bus,
@ -219,6 +221,7 @@ impl Screen {
default_mode_info: mode_info, default_mode_info: mode_info,
draw_pane_frames, draw_pane_frames,
session_is_mirrored, session_is_mirrored,
copy_command,
} }
} }
@ -491,6 +494,7 @@ impl Screen {
self.connected_clients.clone(), self.connected_clients.clone(),
self.session_is_mirrored, self.session_is_mirrored,
client_id, client_id,
self.copy_command.clone(),
); );
tab.apply_layout(layout, new_pids, tab_index, client_id); tab.apply_layout(layout, new_pids, tab_index, client_id);
if self.session_is_mirrored { if self.session_is_mirrored {
@ -692,6 +696,7 @@ pub(crate) fn screen_thread_main(
), ),
draw_pane_frames, draw_pane_frames,
session_is_mirrored, session_is_mirrored,
config_options.copy_command,
); );
loop { loop {
let (event, mut err_ctx) = screen let (event, mut err_ctx) = screen

View file

@ -0,0 +1,39 @@
use std::io::prelude::*;
use std::process::{Command, Stdio};
pub struct CopyCommand {
command: String,
args: Vec<String>,
}
impl CopyCommand {
pub fn new(command: String) -> Self {
let mut command_with_args = command.split(' ').map(String::from);
Self {
command: command_with_args.next().expect("missing command"),
args: command_with_args.collect(),
}
}
pub fn set(&self, value: String) -> bool {
let process = match 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,
};
match process.stdin.unwrap().write_all(value.as_bytes()) {
Err(why) => {
eprintln!("couldn't write to {} stdin: {}", self.command, why);
false
}
Ok(_) => true,
}
}
}

View file

@ -1,9 +1,11 @@
//! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size, //! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size,
//! as well as how they should be resized //! as well as how they should be resized
mod copy_command;
mod pane_grid; mod pane_grid;
mod pane_resizer; mod pane_resizer;
use copy_command::CopyCommand;
use zellij_utils::position::{Column, Line}; use zellij_utils::position::{Column, Line};
use zellij_utils::{position::Position, serde, zellij_tile}; use zellij_utils::{position::Position, serde, zellij_tile};
@ -119,6 +121,7 @@ pub(crate) struct Tab {
session_is_mirrored: bool, session_is_mirrored: bool,
pending_vte_events: HashMap<RawFd, Vec<VteBytes>>, pending_vte_events: HashMap<RawFd, Vec<VteBytes>>,
selecting_with_mouse: bool, selecting_with_mouse: bool,
copy_command: Option<String>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -299,6 +302,7 @@ impl Tab {
connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>, connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>,
session_is_mirrored: bool, session_is_mirrored: bool,
client_id: ClientId, client_id: ClientId,
copy_command: Option<String>,
) -> Self { ) -> Self {
let panes = BTreeMap::new(); let panes = BTreeMap::new();
@ -335,6 +339,7 @@ impl Tab {
connected_clients_in_app, connected_clients_in_app,
connected_clients, connected_clients,
selecting_with_mouse: false, selecting_with_mouse: false,
copy_command,
} }
} }
@ -1912,11 +1917,20 @@ impl Tab {
fn write_selection_to_clipboard(&self, selection: &str) { fn write_selection_to_clipboard(&self, selection: &str) {
let mut output = Output::default(); let mut output = Output::default();
let mut system_clipboard_failure = false;
output.add_clients(&self.connected_clients); 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( output.push_str_to_multiple_clients(
&format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)), &format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)),
self.connected_clients.iter().copied(), self.connected_clients.iter().copied(),
); );
}
}
// TODO: ideally we should be sending the Render instruction from the screen // TODO: ideally we should be sending the Render instruction from the screen
self.senders self.senders
@ -1926,7 +1940,11 @@ impl Tab {
.send_to_plugin(PluginInstruction::Update( .send_to_plugin(PluginInstruction::Update(
None, None,
None, None,
Event::CopyToClipboard, if system_clipboard_failure {
Event::SystemClipboardFailure
} else {
Event::CopyToClipboard
},
)) ))
.unwrap(); .unwrap();
} }

View file

@ -96,6 +96,7 @@ fn create_new_tab(size: Size) -> Tab {
let mut connected_clients = HashSet::new(); let mut connected_clients = HashSet::new();
connected_clients.insert(client_id); connected_clients.insert(client_id);
let connected_clients = Rc::new(RefCell::new(connected_clients)); let connected_clients = Rc::new(RefCell::new(connected_clients));
let copy_command = None;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -110,6 +111,7 @@ fn create_new_tab(size: Size) -> Tab {
connected_clients, connected_clients,
session_is_mirrored, session_is_mirrored,
client_id, client_id,
copy_command,
); );
tab.apply_layout( tab.apply_layout(
LayoutTemplate::default().try_into().unwrap(), LayoutTemplate::default().try_into().unwrap(),

View file

@ -91,6 +91,7 @@ fn create_new_screen(size: Size) -> Screen {
let mode_info = ModeInfo::default(); let mode_info = ModeInfo::default();
let draw_pane_frames = false; let draw_pane_frames = false;
let session_is_mirrored = true; let session_is_mirrored = true;
let copy_command = None;
Screen::new( Screen::new(
bus, bus,
&client_attributes, &client_attributes,
@ -98,6 +99,7 @@ fn create_new_screen(size: Size) -> Screen {
mode_info, mode_info,
draw_pane_frames, draw_pane_frames,
session_is_mirrored, session_is_mirrored,
copy_command,
) )
} }

View file

@ -76,6 +76,7 @@ pub enum Event {
Mouse(Mouse), Mouse(Mouse),
Timer(f64), Timer(f64),
CopyToClipboard, CopyToClipboard,
SystemClipboardFailure,
InputReceived, InputReceived,
Visible(bool), Visible(bool),
} }

View file

@ -74,6 +74,11 @@ pub struct Options {
pub on_force_close: Option<OnForceClose>, pub on_force_close: Option<OnForceClose>,
#[structopt(long)] #[structopt(long)]
pub scroll_buffer_size: Option<usize>, pub scroll_buffer_size: Option<usize>,
/// Switch to using a user supplied command for clipboard instead of OSC52
#[structopt(long)]
#[serde(default)]
pub copy_command: Option<String>,
} }
impl Options { impl Options {
@ -99,6 +104,7 @@ impl Options {
let theme = other.theme.or_else(|| self.theme.clone()); let theme = other.theme.or_else(|| self.theme.clone());
let on_force_close = other.on_force_close.or(self.on_force_close); 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 scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
Options { Options {
simplified_ui, simplified_ui,
@ -111,6 +117,7 @@ impl Options {
mirror_session, mirror_session,
on_force_close, on_force_close,
scroll_buffer_size, scroll_buffer_size,
copy_command,
} }
} }
@ -140,6 +147,7 @@ impl Options {
let theme = other.theme.or_else(|| self.theme.clone()); let theme = other.theme.or_else(|| self.theme.clone());
let on_force_close = other.on_force_close.or(self.on_force_close); 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 scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
Options { Options {
simplified_ui, simplified_ui,
@ -152,6 +160,7 @@ impl Options {
mirror_session, mirror_session,
on_force_close, on_force_close,
scroll_buffer_size, scroll_buffer_size,
copy_command,
} }
} }
@ -200,6 +209,7 @@ impl From<CliOptions> for Options {
mirror_session: opts.mirror_session, mirror_session: opts.mirror_session,
on_force_close: opts.on_force_close, on_force_close: opts.on_force_close,
scroll_buffer_size: opts.scroll_buffer_size, scroll_buffer_size: opts.scroll_buffer_size,
copy_command: opts.copy_command,
} }
} }
} }