use enigo::{Direction, Key, Keyboard};
use std::env;
use std::fs::{self, Permissions};
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use tempfile::NamedTempFile;
use crate::expansion::type_text_with_formatting;
use crate::keyboard::{create_keyboard_controller, send_backspace};
use crate::{Result, SniptError};
pub fn is_url(content: &str) -> bool {
let content = content.trim();
if content.starts_with("http://")
|| content.starts_with("https://")
|| content.starts_with("www.")
{
return true;
}
if content.contains('.') {
let parts: Vec<&str> = content
.split('/')
.next()
.unwrap()
.split(':')
.next()
.unwrap()
.split('.')
.collect();
if parts.len() >= 2 {
let last_part = parts.last().unwrap();
if last_part.len() >= 2
&& last_part.len() <= 63
&& last_part.chars().all(|c| c.is_alphanumeric() || c == '-')
{
return true;
}
}
}
false
}
fn is_script(content: &str) -> bool {
content.trim().starts_with("#!")
}
pub fn execute_snippet(
to_delete: usize,
content: &str,
params: Option<&Vec<String>>,
) -> Result<()> {
let mut keyboard = create_keyboard_controller()?;
send_backspace(&mut keyboard, to_delete)?;
if content.trim().is_empty() {
return Err(SniptError::Other(
"Cannot execute empty content".to_string(),
));
}
thread::sleep(Duration::from_millis(10));
if is_url(content) {
let url = content.to_string();
#[cfg(target_os = "macos")]
{
let _ = Command::new("open")
.arg(&url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
Ok(())
}
#[cfg(target_os = "linux")]
{
let _ = Command::new("xdg-open")
.arg(&url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
Ok(())
}
#[cfg(target_os = "windows")]
{
let _ = Command::new("cmd")
.args(&["/c", "start", "", &url])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
Ok(())
}
} else if is_script(content) {
return execute_script(&mut keyboard, content, params);
} else if content.contains('\n') || content.contains(';') {
return execute_command(&mut keyboard, content, params);
} else {
if let Some(params) = params {
let mut formatted_content = content.to_string();
for (i, param) in params.iter().enumerate() {
let param_marker = format!("${}", i + 1);
formatted_content = formatted_content.replace(¶m_marker, param);
}
if formatted_content.contains("${") && formatted_content.contains("}") {
let modified_content = format!("#!/bin/bash\necho \"{}\"", formatted_content);
return execute_script(&mut keyboard, &modified_content, None);
}
return type_text_with_formatting(&mut keyboard, &formatted_content);
} else {
return type_text_with_formatting(&mut keyboard, content);
}
}
}
fn execute_command(
keyboard: &mut impl Keyboard,
command: &str,
params: Option<&Vec<String>>,
) -> Result<()> {
let command = if let Some(params) = params {
apply_parameter_substitution(command, params)
} else {
command.to_string()
};
#[cfg(target_os = "windows")]
let mut cmd = Command::new("cmd");
#[cfg(target_os = "windows")]
cmd.args(["/c", &command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
#[cfg(not(target_os = "windows"))]
let mut cmd = {
let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
let mut command_obj = Command::new(&shell);
command_obj
.args(["-c", &command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
command_obj
};
let output = match cmd.output() {
Ok(output) => output,
Err(e) => return Err(SniptError::Io(e)),
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let trimmed_stdout = stdout.trim_end().to_string();
thread::sleep(Duration::from_millis(10));
type_text_with_formatting(keyboard, &trimmed_stdout)
} else {
Err(SniptError::Other(format!(
"Command failed: {}",
String::from_utf8_lossy(&output.stderr)
)))
}
}
fn execute_script(
keyboard: &mut impl Keyboard,
script_content: &str,
params: Option<&Vec<String>>,
) -> Result<()> {
let script_content = if let Some(params) = params {
apply_parameter_substitution(script_content, params)
} else {
script_content.to_string()
};
let mut file = NamedTempFile::new()?;
file.write_all(script_content.as_bytes())?;
file.flush()?;
let path = file.path().to_path_buf();
#[cfg(not(target_os = "windows"))]
{
fs::set_permissions(&path, Permissions::from_mode(0o755))?;
}
#[cfg(target_os = "windows")]
let mut cmd = Command::new("cmd");
#[cfg(target_os = "windows")]
cmd.args(["/c", path.to_str().unwrap()])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
#[cfg(not(target_os = "windows"))]
let mut cmd = Command::new(&path);
#[cfg(not(target_os = "windows"))]
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
let output = match cmd.output() {
Ok(output) => output,
Err(e) => return Err(SniptError::Io(e)),
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let trimmed_stdout = stdout.trim_end().to_string();
if trimmed_stdout.contains('\n') {
#[cfg(not(target_os = "windows"))]
{
let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let output_path = format!("{}/snipt_output.txt", home_dir);
fs::write(&output_path, &trimmed_stdout)?;
#[cfg(unix)]
fs::set_permissions(&output_path, Permissions::from_mode(0o644))?;
let cat_cmd = format!("cat \"{}\"", output_path);
thread::sleep(Duration::from_millis(10));
match keyboard.text(&cat_cmd) {
Ok(_) => {}
Err(err) => {
return Err(SniptError::Enigo(format!("Failed to type text: {}", err)))
}
}
match keyboard.key(Key::Return, Direction::Click) {
Ok(_) => {}
Err(err) => {
return Err(SniptError::Enigo(format!("Failed to type key: {}", err)))
}
}
let path_to_delete = output_path.clone();
thread::spawn(move || {
thread::sleep(Duration::from_secs(2));
let _ = fs::remove_file(path_to_delete); });
return Ok(());
}
}
thread::sleep(Duration::from_millis(10));
type_text_with_formatting(keyboard, &trimmed_stdout)
} else {
Err(SniptError::Other(format!(
"Script failed: {}",
String::from_utf8_lossy(&output.stderr)
)))
}
}
fn apply_parameter_substitution(content: &str, params: &[String]) -> String {
let mut result = content.to_string();
for (i, param) in params.iter().enumerate() {
let param_marker = format!("${}", i + 1);
result = result.replace(¶m_marker, param);
}
result = result.replace("$*", ¶ms.join(" "));
result
}