use super::{arboard_provider::with_clipboard_instance, provider::Clipboard};
use nu_protocol::{
Config, ShellError, Value,
engine::{EngineState, Stack},
shell_error::generic::GenericError,
};
use std::sync::{OnceLock, mpsc};
use std::thread;
pub(crate) struct ClipBoardLinux {
use_resident_thread: bool,
}
enum ResidentThreadMessage {
SetText(String, mpsc::Sender<Result<(), String>>),
}
struct ClipboardResidentThread {
tx: mpsc::Sender<ResidentThreadMessage>,
}
impl ClipboardResidentThread {
fn global() -> &'static ClipboardResidentThread {
static CLIPBOARD_THREAD: OnceLock<ClipboardResidentThread> = OnceLock::new();
CLIPBOARD_THREAD.get_or_init(Self::start)
}
fn start() -> ClipboardResidentThread {
let (tx, rx) = mpsc::channel::<ResidentThreadMessage>();
thread::Builder::new()
.name("nu-clipboard-holder".into())
.spawn(move || {
let clipboard = arboard::Clipboard::new();
let mut clipboard = match clipboard {
Ok(clipboard) => clipboard,
Err(err) => {
while let Ok(ResidentThreadMessage::SetText(_, ack_tx)) = rx.recv() {
let _ = ack_tx.send(Err(err.to_string()));
}
return;
}
};
while let Ok(ResidentThreadMessage::SetText(text, ack_tx)) = rx.recv() {
let result = clipboard
.set_text(text)
.map_err(|err| err.to_string())
.map(|_| ());
let _ = ack_tx.send(result);
}
})
.expect("clipboard background thread failed to start");
ClipboardResidentThread { tx }
}
fn copy_text(&self, text: &str) -> Result<(), ShellError> {
let (ack_tx, ack_rx) = mpsc::channel();
self.tx
.send(ResidentThreadMessage::SetText(text.to_owned(), ack_tx))
.map_err(|err| {
ShellError::Generic(GenericError::new_internal(
"Clipboard thread channel failed.",
err.to_string(),
))
})?;
let result = ack_rx.recv().map_err(|err| {
ShellError::Generic(GenericError::new_internal(
"Clipboard thread failed.",
err.to_string(),
))
})?;
result.map_err(|err| {
ShellError::Generic(GenericError::new_internal("Clipboard thread failed.", err))
})?;
Ok(())
}
}
impl ClipBoardLinux {
pub fn new(config: &Config, engine_state: &EngineState, stack: &mut Stack) -> Self {
Self {
use_resident_thread: should_use_resident_thread(config, engine_state, stack),
}
}
}
impl Clipboard for ClipBoardLinux {
fn copy_text(&self, text: &str) -> Result<(), ShellError> {
if self.use_resident_thread {
ClipboardResidentThread::global().copy_text(text)
} else {
with_clipboard_instance(|clip: &mut arboard::Clipboard| clip.set_text(text))
}
}
fn get_text(&self) -> Result<String, ShellError> {
with_clipboard_instance(|clip| clip.get_text())
}
}
fn should_use_resident_thread(
config: &Config,
engine_state: &EngineState,
stack: &mut Stack,
) -> bool {
if config.clip.resident_mode {
return true;
}
if let Some(no_resident) = read_no_resident_legacy(
crate::platform::clip::get_config::get_clip_config_with_plugin_fallback(
engine_state,
stack,
)
.as_ref(),
) {
return !no_resident;
}
true
}
fn read_no_resident_legacy(value: Option<&Value>) -> Option<bool> {
match value {
None => None,
Some(Value::Record { val, .. }) => {
if let Some(value) = val
.get("NO_RESIDENT")
.or_else(|| val.get("no_resident"))
.or_else(|| val.get("noResident"))
{
read_no_resident_legacy(Some(value))
} else {
None
}
}
Some(Value::Bool { val, .. }) => Some(*val),
Some(Value::String { val, .. }) => match val.as_str() {
"true" | "True" | "1" => Some(true),
"false" | "False" | "0" => Some(false),
_ => None,
},
Some(Value::Int { val, .. }) => Some(*val == 1),
_ => None,
}
}