use std::{cell::RefCell, process::Command};
use arboard::Clipboard;
use crate::config::ClipboardConfig;
const USER_COMMAND_MARKER_PREFIX: &str = "{{";
const USER_COMMAND_TARGET_HASH_MARKER: &str = "{{target_hash}}";
const USER_COMMAND_FIRST_PARENT_HASH_MARKER: &str = "{{first_parent_hash}}";
const USER_COMMAND_PARENT_HASHES_MARKER: &str = "{{parent_hashes}}";
const USER_COMMAND_REFS_MARKER: &str = "{{refs}}";
const USER_COMMAND_BRANCHES_MARKER: &str = "{{branches}}";
const USER_COMMAND_REMOTE_BRANCHES_MARKER: &str = "{{remote_branches}}";
const USER_COMMAND_TAGS_MARKER: &str = "{{tags}}";
const USER_COMMAND_AREA_WIDTH_MARKER: &str = "{{area_width}}";
const USER_COMMAND_AREA_HEIGHT_MARKER: &str = "{{area_height}}";
thread_local! {
static CLIPBOARD: RefCell<Option<Clipboard>> = const { RefCell::new(None) };
}
pub fn copy_to_clipboard(value: String, config: &ClipboardConfig) -> Result<(), String> {
match config {
ClipboardConfig::Auto => copy_to_clipboard_auto(value),
ClipboardConfig::Custom { commands } => copy_to_clipboard_custom(value, commands),
}
}
fn copy_to_clipboard_custom(value: String, commands: &[String]) -> Result<(), String> {
use std::io::Write;
use std::process::Stdio;
if commands.is_empty() {
return Err("No clipboard command specified".to_string());
}
let mut child = Command::new(&commands[0])
.args(&commands[1..])
.stdin(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to run {}: {e}", commands[0]))?;
child
.stdin
.take()
.expect("stdin should be available")
.write_all(value.as_bytes())
.map_err(|e| format!("Failed to write to {}: {e}", commands[0]))?;
child
.wait()
.map_err(|e| format!("{} failed: {e}", commands[0]))?;
Ok(())
}
fn copy_to_clipboard_auto(value: String) -> Result<(), String> {
CLIPBOARD.with_borrow_mut(|clipboard| {
if clipboard.is_none() {
*clipboard = Clipboard::new()
.map(Some)
.map_err(|e| format!("Failed to create clipboard: {e:?}"))?;
}
clipboard
.as_mut()
.expect("The clipboard should have been initialized above")
.set_text(value)
.map_err(|e| format!("Failed to copy to clipboard: {e:?}"))
})
}
pub struct ExternalCommandParameters<'a> {
pub command: &'a [String],
pub target_hash: &'a str,
pub parent_hashes: Vec<&'a str>,
pub all_refs: Vec<&'a str>,
pub branches: Vec<&'a str>,
pub remote_branches: Vec<&'a str>,
pub tags: Vec<&'a str>,
pub area_width: u16,
pub area_height: u16,
}
pub fn exec_user_command(params: ExternalCommandParameters) -> Result<String, String> {
let command = build_user_command(¶ms);
let output = Command::new(&command[0])
.args(&command[1..])
.output()
.map_err(|e| format!("Failed to execute command: {e:?}"))?;
if !output.status.success() {
let msg = format!(
"Command exited with non-zero status: {}, stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
return Err(msg);
}
Ok(String::from_utf8_lossy(&output.stdout).into())
}
pub fn exec_user_command_suspend(params: ExternalCommandParameters) -> Result<(), String> {
let command = build_user_command(¶ms);
let output = Command::new(&command[0])
.args(&command[1..])
.status()
.map_err(|e| format!("Failed to execute command: {e:?}"))?;
if !output.success() {
let msg = format!("Command exited with non-zero status: {output}");
return Err(msg);
}
Ok(())
}
fn build_user_command(params: &ExternalCommandParameters) -> Vec<String> {
fn to_vec(ss: &[&str]) -> Vec<String> {
ss.iter().map(|s| s.to_string()).collect()
}
let mut command = Vec::new();
for arg in params.command {
if !arg.contains(USER_COMMAND_MARKER_PREFIX) {
command.push(arg.clone());
continue;
}
match arg.as_str() {
USER_COMMAND_BRANCHES_MARKER => command.extend(to_vec(¶ms.branches)),
USER_COMMAND_REMOTE_BRANCHES_MARKER => command.extend(to_vec(¶ms.remote_branches)),
USER_COMMAND_TAGS_MARKER => command.extend(to_vec(¶ms.tags)),
USER_COMMAND_REFS_MARKER => command.extend(to_vec(¶ms.all_refs)),
USER_COMMAND_PARENT_HASHES_MARKER => command.extend(to_vec(¶ms.parent_hashes)),
_ => command.push(replace_command_arg(arg, params)),
}
}
command
}
fn replace_command_arg(s: &str, params: &ExternalCommandParameters) -> String {
let sep = " ";
let target_hash = params.target_hash;
let first_parent_hash = ¶ms.parent_hashes.first().cloned().unwrap_or_default();
let parent_hashes = ¶ms.parent_hashes.join(sep);
let all_refs = ¶ms.all_refs.join(sep);
let branches = ¶ms.branches.join(sep);
let remote_branches = ¶ms.remote_branches.join(sep);
let tags = ¶ms.tags.join(sep);
let area_width = ¶ms.area_width.to_string();
let area_height = ¶ms.area_height.to_string();
s.replace(USER_COMMAND_TARGET_HASH_MARKER, target_hash)
.replace(USER_COMMAND_FIRST_PARENT_HASH_MARKER, first_parent_hash)
.replace(USER_COMMAND_PARENT_HASHES_MARKER, parent_hashes)
.replace(USER_COMMAND_REFS_MARKER, all_refs)
.replace(USER_COMMAND_BRANCHES_MARKER, branches)
.replace(USER_COMMAND_REMOTE_BRANCHES_MARKER, remote_branches)
.replace(USER_COMMAND_TAGS_MARKER, tags)
.replace(USER_COMMAND_AREA_WIDTH_MARKER, area_width)
.replace(USER_COMMAND_AREA_HEIGHT_MARKER, area_height)
}