code-moniker 0.4.0

Standalone CLI / linter for the code-moniker symbol graph: file and directory probes, project-wide architecture rules.
Documentation
use std::io::{ErrorKind, Write};
use std::process::{Command, Stdio};
use std::thread;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::ui) struct ClipboardResult {
	pub(in crate::ui) component: String,
	pub(in crate::ui) result: Result<(), String>,
}

pub(super) fn copy_text_async<F>(component: String, text: String, notify: F) -> anyhow::Result<()>
where
	F: FnOnce(ClipboardResult) + Send + 'static,
{
	if text.is_empty() {
		return Err(anyhow::anyhow!("clipboard snapshot is empty"));
	}
	thread::Builder::new()
		.name("code-moniker-clipboard".to_string())
		.spawn(move || {
			let result = copy_with_system_clipboard(&text).map_err(|error| format!("{error:#}"));
			notify(ClipboardResult { component, result });
		})?;
	Ok(())
}

#[cfg(target_os = "macos")]
fn copy_with_system_clipboard(text: &str) -> anyhow::Result<()> {
	copy_with_command("pbcopy", &[], text)
}

#[cfg(target_os = "windows")]
fn copy_with_system_clipboard(text: &str) -> anyhow::Result<()> {
	copy_with_command("clip", &[], text)
}

#[cfg(all(unix, not(target_os = "macos")))]
fn copy_with_system_clipboard(text: &str) -> anyhow::Result<()> {
	let candidates: &[(&str, &[&str])] = &[
		("wl-copy", &[]),
		("xclip", &["-selection", "clipboard"]),
		("xsel", &["--clipboard", "--input"]),
	];
	let mut last_error = None;
	for (program, args) in candidates {
		match copy_with_command(program, args, text) {
			Ok(()) => return Ok(()),
			Err(error) => last_error = Some(error),
		}
	}
	Err(last_error.unwrap_or_else(|| anyhow::anyhow!("no clipboard command available")))
}

fn copy_with_command(program: &str, args: &[&str], text: &str) -> anyhow::Result<()> {
	let mut child = match Command::new(program)
		.args(args)
		.stdin(Stdio::piped())
		.stdout(Stdio::null())
		.stderr(Stdio::null())
		.spawn()
	{
		Ok(child) => child,
		Err(error) if error.kind() == ErrorKind::NotFound => {
			return Err(anyhow::anyhow!("{program} not found"));
		}
		Err(error) => return Err(error.into()),
	};
	let Some(mut stdin) = child.stdin.take() else {
		return Err(anyhow::anyhow!("cannot open {program} stdin"));
	};
	if let Err(error) = stdin.write_all(text.as_bytes()) {
		let _ = child.kill();
		let _ = child.wait();
		return Err(error.into());
	}
	drop(stdin);
	let status = child.wait()?;
	if !status.success() {
		return Err(anyhow::anyhow!("{program} exited with {status}"));
	}
	Ok(())
}