use crate::config::{Config, ShellType};
use std::fs;
use std::path::{Path, PathBuf};
const BASH_SCRIPT: &str = include_str!("../shell_integration/par_term_shell_integration.bash");
const ZSH_SCRIPT: &str = include_str!("../shell_integration/par_term_shell_integration.zsh");
const FISH_SCRIPT: &str = include_str!("../shell_integration/par_term_shell_integration.fish");
const PT_DL_SCRIPT: &str = include_str!("../shell_integration/pt-dl");
const PT_UL_SCRIPT: &str = include_str!("../shell_integration/pt-ul");
const PT_IMGCAT_SCRIPT: &str = include_str!("../shell_integration/pt-imgcat");
const MARKER_START: &str = "# >>> par-term shell integration >>>";
const MARKER_END: &str = "# <<< par-term shell integration <<<";
#[derive(Debug)]
pub struct InstallResult {
pub shell: ShellType,
pub script_path: PathBuf,
pub rc_file: PathBuf,
pub needs_restart: bool,
}
#[derive(Debug, Default)]
pub struct UninstallResult {
pub cleaned: Vec<PathBuf>,
pub needs_manual: Vec<PathBuf>,
pub scripts_removed: Vec<PathBuf>,
}
fn install_utilities() -> Result<PathBuf, String> {
let bin_dir = Config::shell_integration_dir().join("bin");
fs::create_dir_all(&bin_dir)
.map_err(|e| format!("Failed to create bin directory {:?}: {}", bin_dir, e))?;
let utilities: &[(&str, &str)] = &[
("pt-dl", PT_DL_SCRIPT),
("pt-ul", PT_UL_SCRIPT),
("pt-imgcat", PT_IMGCAT_SCRIPT),
];
for (name, content) in utilities {
let path = bin_dir.join(name);
fs::write(&path, content).map_err(|e| format!("Failed to write {:?}: {}", path, e))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
fs::set_permissions(&path, perms)
.map_err(|e| format!("Failed to set permissions on {:?}: {}", path, e))?;
}
}
Ok(bin_dir)
}
pub fn install(shell: Option<ShellType>) -> Result<InstallResult, String> {
let shell = shell.unwrap_or_else(detected_shell);
if shell == ShellType::Unknown {
return Err(
"Could not detect shell type. Please specify shell manually (bash, zsh, or fish)."
.to_string(),
);
}
let script_content = get_script_content(shell);
let integration_dir = Config::shell_integration_dir();
fs::create_dir_all(&integration_dir)
.map_err(|e| format!("Failed to create directory {:?}: {}", integration_dir, e))?;
let script_filename = format!("shell_integration.{}", shell.extension());
let script_path = integration_dir.join(&script_filename);
fs::write(&script_path, script_content)
.map_err(|e| format!("Failed to write script to {:?}: {}", script_path, e))?;
install_utilities()?;
let rc_file = get_rc_file(shell)?;
add_to_rc_file(&rc_file, shell)?;
Ok(InstallResult {
shell,
script_path,
rc_file,
needs_restart: true,
})
}
pub fn uninstall() -> Result<UninstallResult, String> {
let mut result = UninstallResult::default();
for shell in [ShellType::Bash, ShellType::Zsh, ShellType::Fish] {
if let Ok(rc_file) = get_rc_file(shell)
&& rc_file.exists()
{
match remove_from_rc_file(&rc_file) {
Ok(true) => result.cleaned.push(rc_file),
Ok(false) => { }
Err(_) => result.needs_manual.push(rc_file),
}
}
}
let integration_dir = Config::shell_integration_dir();
for shell in [ShellType::Bash, ShellType::Zsh, ShellType::Fish] {
let script_filename = format!("shell_integration.{}", shell.extension());
let script_path = integration_dir.join(&script_filename);
if script_path.exists() && fs::remove_file(&script_path).is_ok() {
result.scripts_removed.push(script_path);
}
}
let bin_dir = integration_dir.join("bin");
if bin_dir.exists() {
let _ = fs::remove_dir_all(&bin_dir);
}
Ok(result)
}
pub fn is_installed() -> bool {
let shell = detected_shell();
if shell == ShellType::Unknown {
return false;
}
let integration_dir = Config::shell_integration_dir();
let script_filename = format!("shell_integration.{}", shell.extension());
let script_path = integration_dir.join(&script_filename);
if !script_path.exists() {
return false;
}
if let Ok(rc_file) = get_rc_file(shell)
&& let Ok(content) = fs::read_to_string(&rc_file)
{
return content.contains(MARKER_START) && content.contains(MARKER_END);
}
false
}
pub fn detected_shell() -> ShellType {
ShellType::detect()
}
fn get_script_content(shell: ShellType) -> &'static str {
match shell {
ShellType::Bash => BASH_SCRIPT,
ShellType::Zsh => ZSH_SCRIPT,
ShellType::Fish => FISH_SCRIPT,
ShellType::Unknown => BASH_SCRIPT, }
}
fn get_rc_file(shell: ShellType) -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
let rc_file = match shell {
ShellType::Bash => {
let bashrc = home.join(".bashrc");
let bash_profile = home.join(".bash_profile");
if bashrc.exists() {
bashrc
} else {
bash_profile
}
}
ShellType::Zsh => home.join(".zshrc"),
ShellType::Fish => {
let xdg_config = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| home.join(".config"));
xdg_config.join("fish").join("config.fish")
}
ShellType::Unknown => return Err("Unknown shell type".to_string()),
};
Ok(rc_file)
}
fn add_to_rc_file(rc_file: &Path, shell: ShellType) -> Result<(), String> {
let existing_content = if rc_file.exists() {
fs::read_to_string(rc_file).map_err(|e| format!("Failed to read {:?}: {}", rc_file, e))?
} else {
if let Some(parent) = rc_file.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create directory {:?}: {}", parent, e))?;
}
String::new()
};
if existing_content.contains(MARKER_START) {
let cleaned = remove_marker_block(&existing_content);
let new_content = format!("{}\n{}", cleaned.trim_end(), generate_source_block(shell));
fs::write(rc_file, new_content)
.map_err(|e| format!("Failed to write {:?}: {}", rc_file, e))?;
} else {
let new_content = if existing_content.is_empty() {
generate_source_block(shell)
} else if existing_content.ends_with('\n') {
format!("{}\n{}", existing_content, generate_source_block(shell))
} else {
format!("{}\n\n{}", existing_content, generate_source_block(shell))
};
fs::write(rc_file, new_content)
.map_err(|e| format!("Failed to write {:?}: {}", rc_file, e))?;
}
Ok(())
}
fn remove_from_rc_file(rc_file: &Path) -> Result<bool, String> {
let content =
fs::read_to_string(rc_file).map_err(|e| format!("Failed to read {:?}: {}", rc_file, e))?;
if !content.contains(MARKER_START) {
return Ok(false);
}
let cleaned = remove_marker_block(&content);
if cleaned != content {
fs::write(rc_file, &cleaned)
.map_err(|e| format!("Failed to write {:?}: {}", rc_file, e))?;
}
Ok(true)
}
fn home_relative_str(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(rel) = path.strip_prefix(&home)
{
return format!("$HOME/{}", rel.display());
}
path.display().to_string()
}
fn generate_source_block(shell: ShellType) -> String {
let integration_dir = Config::shell_integration_dir();
let script_filename = format!("shell_integration.{}", shell.extension());
let script_path = integration_dir.join(&script_filename);
let bin_dir = integration_dir.join("bin");
let script_path_str = home_relative_str(&script_path);
let bin_dir_str = home_relative_str(&bin_dir);
match shell {
ShellType::Fish => {
format!(
"{}\nif test -d \"{}\"\n set -gx PATH \"{}\" $PATH\nend\nif test -f \"{}\"\n source \"{}\"\nend\n{}\n",
MARKER_START,
bin_dir_str,
bin_dir_str,
script_path_str,
script_path_str,
MARKER_END
)
}
_ => {
format!(
"{}\nif [ -d \"{}\" ]; then\n export PATH=\"{}:$PATH\"\nfi\nif [ -f \"{}\" ]; then\n source \"{}\"\nfi\n{}\n",
MARKER_START,
bin_dir_str,
bin_dir_str,
script_path_str,
script_path_str,
MARKER_END
)
}
}
}
fn remove_marker_block(content: &str) -> String {
let mut result = String::new();
let mut in_block = false;
let mut found_block = false;
for line in content.lines() {
if line.trim() == MARKER_START {
in_block = true;
found_block = true;
continue;
}
if line.trim() == MARKER_END {
in_block = false;
continue;
}
if !in_block {
result.push_str(line);
result.push('\n');
}
}
if found_block {
let trimmed = result.trim_end();
if trimmed.is_empty() {
String::new()
} else {
format!("{}\n", trimmed)
}
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remove_marker_block() {
let content = format!(
"# existing content\n{}\nsource something\n{}\n# more content\n",
MARKER_START, MARKER_END
);
let result = remove_marker_block(&content);
assert!(!result.contains(MARKER_START));
assert!(!result.contains(MARKER_END));
assert!(result.contains("# existing content"));
assert!(result.contains("# more content"));
assert!(!result.contains("source something"));
}
#[test]
fn test_remove_marker_block_no_markers() {
let content = "# just some content\nno markers here\n";
let result = remove_marker_block(content);
assert_eq!(result, content);
}
#[test]
fn test_generate_source_block_bash() {
let block = generate_source_block(ShellType::Bash);
assert!(block.contains(MARKER_START));
assert!(block.contains(MARKER_END));
assert!(block.contains("source"));
assert!(block.contains(".bash"));
assert!(block.contains("export PATH="));
assert!(block.contains("$HOME/"));
}
#[test]
fn test_generate_source_block_zsh() {
let block = generate_source_block(ShellType::Zsh);
assert!(block.contains(MARKER_START));
assert!(block.contains(MARKER_END));
assert!(block.contains("source"));
assert!(block.contains(".zsh"));
assert!(block.contains("export PATH="));
assert!(block.contains("$HOME/"));
}
#[test]
fn test_generate_source_block_fish() {
let block = generate_source_block(ShellType::Fish);
assert!(block.contains(MARKER_START));
assert!(block.contains(MARKER_END));
assert!(block.contains("source"));
assert!(block.contains(".fish"));
assert!(block.contains("if test -f"));
assert!(block.contains("end"));
assert!(block.contains("set -gx PATH"));
assert!(block.contains("$HOME/"));
}
#[test]
fn test_get_script_content() {
assert!(!get_script_content(ShellType::Bash).is_empty());
assert!(!get_script_content(ShellType::Zsh).is_empty());
assert!(!get_script_content(ShellType::Fish).is_empty());
}
#[test]
fn test_detected_shell() {
let _shell = detected_shell();
}
#[test]
fn test_utility_scripts_embedded() {
assert!(!PT_DL_SCRIPT.is_empty());
assert!(!PT_UL_SCRIPT.is_empty());
assert!(!PT_IMGCAT_SCRIPT.is_empty());
assert!(PT_DL_SCRIPT.starts_with("#!/bin/sh"));
assert!(PT_UL_SCRIPT.starts_with("#!/bin/sh"));
assert!(PT_IMGCAT_SCRIPT.starts_with("#!/bin/sh"));
}
}