use crate::error::{CliError, Result};
use std::env;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::Command;
#[cfg(target_os = "linux")]
pub fn uninstall() -> Result<()> {
println!("Uninstalling Glyph from Linux system...");
sudo_remove_binary()?;
remove_mime_type()?;
remove_mailcap()?;
remove_shell_function("$HOME/.bashrc")?;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn uninstall() -> Result<()> {
println!("Uninstalling Glyph from macOS system...");
sudo_remove_binary()?;
remove_macos_file_association()?;
remove_shell_function_macos()?;
Ok(())
}
#[cfg(target_os = "windows")]
pub fn uninstall() -> Result<()> {
println!("Uninstalling Glyph from Windows system...");
remove_binary_windows()?;
remove_registry_entries()?;
remove_from_windows_path()?;
remove_shell_function("$HOME/.profile")?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
pub fn uninstall() -> Result<()> {
Err(CliError::Input(
"Uninstallation is not supported on this platform".to_string(),
))
}
fn sudo_remove_binary() -> Result<()> {
Command::new("sudo")
.args(["rm", "-f", "/usr/local/bin/glyph"])
.status()
.map_err(|e| CliError::from(format!("Failed to remove binary: {}", e)))?;
Ok(())
}
#[cfg(target_os = "linux")]
fn remove_mime_type() -> Result<()> {
let status = Command::new("sudo")
.args([
"rm",
"-f",
"/usr/share/mime/packages/application-x-glyph.xml",
])
.status()?;
if !status.success() {
return Err(CliError::from("Failed to remove mime type"));
}
Command::new("sudo")
.args(["update-mime-database", "/usr/share/mime"])
.status()?;
Ok(())
}
#[cfg(target_os = "linux")]
fn remove_mailcap() -> Result<()> {
remove_mailcap_with_path("/etc/mailcap")
}
#[cfg(target_os = "linux")]
fn remove_mailcap_with_path(mailcap_path: &str) -> Result<()> {
if !Command::new("grep")
.args(["-q", "application/x-glyph", mailcap_path])
.status()?
.success()
{
return Ok(());
}
let content = fs::read_to_string(mailcap_path)?;
let new_content: String = content
.lines()
.filter(|line| !line.starts_with("application/x-glyph"))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write("/tmp/mailcap_new", new_content)?;
Command::new("sudo")
.args(["cp", "/tmp/mailcap_new", mailcap_path])
.status()?;
let _ = fs::remove_file("/tmp/mailcap_new"); Ok(())
}
#[cfg(target_os = "macos")]
fn remove_macos_file_association() -> Result<()> {
let output = Command::new("defaults")
.args(["read", "com.apple.LaunchServices", "LSHandlers"])
.output()?;
if String::from_utf8_lossy(&output.stdout).contains("public.glyph") {
Command::new("defaults")
.args(["delete", "com.apple.LaunchServices", "LSHandlers"])
.status()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn remove_shell_function_macos() -> Result<()> {
let home = env::var("HOME")?;
let zshrc_path = PathBuf::from(&home).join(".zshrc");
if !zshrc_path.exists() {
return Ok(());
}
let metadata = fs::metadata(&zshrc_path)?;
let mode = metadata.permissions().mode();
Command::new("chmod")
.args(["0644", zshrc_path.to_string_lossy().as_ref()])
.status()?;
let result = remove_shell_function(&zshrc_path.to_string_lossy());
Command::new("chmod")
.args([
&format!("0{:o}", mode & 0o777),
zshrc_path.to_string_lossy().as_ref(),
])
.status()?;
fix_zsh_completion_permissions()?;
result
}
#[cfg(target_os = "macos")]
fn fix_zsh_completion_permissions() -> Result<()> {
let home = env::var("HOME")?;
let zsh_completion_dir = PathBuf::from(&home).join(".zsh");
if zsh_completion_dir.exists() {
Command::new("chmod")
.args(["0755", zsh_completion_dir.to_string_lossy().as_ref()])
.status()?;
Command::new("chmod")
.args(["-R", "go-w", zsh_completion_dir.to_string_lossy().as_ref()])
.status()?;
}
Command::new("chmod")
.args(["0755", home.as_str()])
.status()?;
Ok(())
}
#[cfg(target_os = "windows")]
fn remove_binary_windows() -> Result<()> {
let home = env::var("HOME")?;
let glyph_exe = PathBuf::from(&home).join("bin").join("glyph.exe");
if glyph_exe.exists() {
fs::remove_file(glyph_exe)?;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn remove_registry_entries() -> Result<()> {
let reg_content = r#"Windows Registry Editor Version 5.00
[-HKEY_CLASSES_ROOT\.glyph]
[-HKEY_CLASSES_ROOT\GlyphFile]"#;
fs::write("glyph-uninstall.reg", reg_content)?;
println!("Please run glyph-uninstall.reg to remove file associations");
Ok(())
}
#[cfg(target_os = "windows")]
fn remove_from_windows_path() -> Result<()> {
let home = env::var("HOME")?;
let profile_path = PathBuf::from(&home).join(".profile");
if profile_path.exists() {
let content = fs::read_to_string(&profile_path)?;
let new_content: String = content
.lines()
.filter(|line| !line.contains("export PATH=$PATH:") || !line.contains("/bin"))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(&profile_path, new_content)?;
}
Ok(())
}
fn remove_shell_function(rc_file: &str) -> Result<()> {
let rc_path = PathBuf::from(rc_file.replace("$HOME", &env::var("HOME")?));
if rc_path.exists() {
let content = fs::read_to_string(&rc_path)?;
let mut new_lines = Vec::new();
let mut skip_lines = false;
let mut brace_count = 0;
for line in content.lines() {
if !skip_lines {
if line.trim().starts_with("function cat()") {
skip_lines = true;
brace_count += line.matches('{').count() as i32;
continue;
}
new_lines.push(line);
} else {
brace_count += line.matches('{').count() as i32;
brace_count -= line.matches('}').count() as i32;
if brace_count == 0 {
skip_lines = false;
}
}
}
if new_lines.len() < content.lines().count() {
let new_content = if new_lines.is_empty() {
String::new()
} else {
new_lines.join("\n") + "\n"
};
fs::write(&rc_path, new_content)?;
println!("Removed cat function override from {}", rc_path.display());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::{tempdir, TempDir};
fn create_mock_rc_with_cat_function(dir: &TempDir) -> PathBuf {
let rc_path = dir.path().join(".bashrc");
let content = r#"
# Some existing content
export PATH=$HOME/bin:$PATH
function cat() {
for arg in "$@"; do
if [[ "${arg}" == *.glyph ]]; then
glyph "${arg}"
else
command cat "${arg}"
fi
done
}
# More content
alias ls='ls --color=auto'
"#;
fs::write(&rc_path, content).unwrap();
rc_path
}
#[cfg(all(test, target_os = "linux"))]
fn setup_mock_system(dir: &TempDir) -> PathBuf {
let usr_local_bin = dir.path().join("usr").join("local").join("bin");
let mime_dir = dir
.path()
.join("usr")
.join("share")
.join("mime")
.join("packages");
fs::create_dir_all(&usr_local_bin).unwrap();
fs::create_dir_all(&mime_dir).unwrap();
fs::write(usr_local_bin.join("glyph"), "mock binary content").unwrap();
fs::write(
mime_dir.join("application-x-glyph.xml"),
"mock mime content",
)
.unwrap();
dir.path().to_path_buf()
}
#[test]
fn test_remove_shell_function_basic() {
let temp = tempdir().unwrap();
let rc_path = create_mock_rc_with_cat_function(&temp);
remove_shell_function(&rc_path.to_string_lossy()).unwrap();
let content = fs::read_to_string(&rc_path).unwrap();
assert!(!content.contains("function cat()"));
assert!(!content.contains("glyph"));
assert!(content.contains("export PATH"));
assert!(content.contains("alias ls"));
}
#[test]
fn test_remove_shell_function_no_function() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join(".bashrc");
fs::write(
&rc_path,
"# Just some content\nexport PATH=$HOME/bin:$PATH\n",
)
.unwrap();
let result = remove_shell_function(&rc_path.to_string_lossy());
assert!(result.is_ok());
let content = fs::read_to_string(&rc_path).unwrap();
assert_eq!(
content,
"# Just some content\nexport PATH=$HOME/bin:$PATH\n"
);
}
#[test]
fn test_remove_shell_function_nonexistent_file() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join("nonexistent.rc");
let result = remove_shell_function(&rc_path.to_string_lossy());
assert!(result.is_ok());
}
#[test]
#[cfg(target_os = "linux")]
fn test_remove_mime_type() {
let temp = tempdir().unwrap();
let root = setup_mock_system(&temp);
let mime_file = root
.join("usr")
.join("share")
.join("mime")
.join("packages")
.join("application-x-glyph.xml");
assert!(mime_file.exists());
fs::remove_file(&mime_file).unwrap();
assert!(!mime_file.exists());
}
#[test]
#[cfg(target_os = "linux")]
fn test_remove_mailcap_entry() {
use std::env;
use std::os::unix::fs::PermissionsExt;
let temp = tempdir().unwrap();
let root = setup_mock_system(&temp);
let etc_dir = root.join("etc");
fs::create_dir_all(&etc_dir).unwrap();
let mailcap = etc_dir.join("mailcap");
let content = r#"
# Other entries
image/jpeg; display %s
application/x-glyph; glyph %s; copiousoutput
text/plain; cat %s
"#;
fs::write(&mailcap, content).unwrap();
let bin_dir = root.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let mock_grep = bin_dir.join("grep");
fs::write(
&mock_grep,
r#"#!/bin/sh
if [ "$1" = "-q" ] && [ "$2" = "application/x-glyph" ]; then
exit 0 # Entry exists
fi
exit 1
"#,
)
.unwrap();
fs::set_permissions(&mock_grep, std::fs::Permissions::from_mode(0o755)).unwrap();
let mock_sudo = bin_dir.join("sudo");
fs::write(
&mock_sudo,
r#"#!/bin/sh
if [ "$1" = "cp" ]; then
cp "$2" "$3"
fi
"#,
)
.unwrap();
fs::set_permissions(&mock_sudo, std::fs::Permissions::from_mode(0o755)).unwrap();
let old_path = env::var("PATH").unwrap_or_default();
env::set_var("PATH", format!("{}:{}", bin_dir.display(), old_path));
let result = remove_mailcap_with_path(&mailcap.to_string_lossy());
assert!(result.is_ok(), "remove_mailcap failed: {:?}", result);
let new_content = fs::read_to_string(&mailcap).unwrap();
assert!(
!new_content.contains("application/x-glyph"),
"mailcap still contains glyph entry"
);
assert!(
new_content.contains("image/jpeg"),
"mailcap missing jpeg entry"
);
assert!(
new_content.contains("text/plain"),
"mailcap missing text entry"
);
assert!(
!PathBuf::from("/tmp/mailcap_new").exists(),
"temporary file not cleaned up"
);
env::set_var("PATH", old_path);
}
#[test]
#[cfg(target_os = "linux")]
fn test_remove_mailcap_entry_nonexistent() {
use std::env;
use std::os::unix::fs::PermissionsExt;
let temp = tempdir().unwrap();
let root = setup_mock_system(&temp);
let etc_dir = root.join("etc");
fs::create_dir_all(&etc_dir).unwrap();
let mailcap = etc_dir.join("mailcap");
let content = r#"
# Other entries only
image/jpeg; display %s
text/plain; cat %s
"#;
fs::write(&mailcap, content).unwrap();
let bin_dir = root.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let mock_grep = bin_dir.join("grep");
fs::write(
&mock_grep,
r#"#!/bin/sh
exit 1 # Entry doesn't exist
"#,
)
.unwrap();
fs::set_permissions(&mock_grep, std::fs::Permissions::from_mode(0o755)).unwrap();
let old_path = env::var("PATH").unwrap_or_default();
env::set_var("PATH", format!("{}:{}", bin_dir.display(), old_path));
let result = remove_mailcap_with_path(&mailcap.to_string_lossy());
assert!(result.is_ok());
let new_content = fs::read_to_string(&mailcap).unwrap();
assert_eq!(content, new_content);
env::set_var("PATH", old_path);
}
#[test]
#[cfg(target_os = "windows")]
fn test_remove_binary_windows() {
let temp = tempdir().unwrap();
let bin_dir = temp.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let glyph_exe = bin_dir.join("glyph.exe");
fs::write(&glyph_exe, "mock binary").unwrap();
assert!(glyph_exe.exists());
fs::remove_file(&glyph_exe).unwrap();
assert!(!glyph_exe.exists());
}
#[test]
#[cfg(target_os = "windows")]
fn test_remove_from_windows_path() {
let temp = tempdir().unwrap();
let profile = temp.path().join(".profile");
let content = r#"
# Other PATH entries
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/home/user/bin
export JAVA_HOME=/usr/lib/jvm/java
"#;
fs::write(&profile, content).unwrap();
remove_from_windows_path().unwrap();
let new_content = fs::read_to_string(&profile).unwrap();
assert!(!new_content.contains("export PATH=$PATH:/home/user/bin"));
assert!(new_content.contains("export PATH=$PATH:/usr/local/bin"));
assert!(new_content.contains("JAVA_HOME"));
}
#[test]
fn test_remove_shell_function_with_nested_braces() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join(".bashrc");
let content = r#"
function cat() {
for arg in "$@"; do
if [[ "${arg}" == *.glyph ]]; then
if [[ -f "${arg}" ]]; then
glyph "${arg}"
}
else
command cat "${arg}"
fi
done
}
"#;
fs::write(&rc_path, content).unwrap();
remove_shell_function(&rc_path.to_string_lossy()).unwrap();
let new_content = fs::read_to_string(&rc_path).unwrap();
assert!(!new_content.contains("function cat()"));
assert!(!new_content.contains("glyph"));
}
#[test]
fn test_remove_shell_function_with_inline_brace() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join(".bashrc");
let content = r#"# Some content
function cat() {
for arg in "$@"; do
if [[ "${arg}" == *.glyph ]]; then
glyph "${arg}"
else
command cat "${arg}"
fi
done
}
# More content
"#;
fs::write(&rc_path, content).unwrap();
remove_shell_function(&rc_path.to_string_lossy()).unwrap();
let new_content = fs::read_to_string(&rc_path).unwrap();
assert!(!new_content.contains("function cat()"));
assert!(!new_content.contains("else"));
assert!(!new_content.contains("command cat"));
assert!(new_content.contains("# Some content"));
assert!(new_content.contains("# More content"));
}
}