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")]
use std::process::Stdio;
#[cfg(target_os = "linux")]
pub fn install() -> Result<()> {
sudo_install_binary()?;
create_mime_type()?;
update_mailcap()?;
add_shell_function("$HOME/.bashrc")?;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn install() -> Result<()> {
sudo_install_binary()?;
setup_macos_file_association()?;
add_shell_function_macos("$HOME/.zshrc")?;
Ok(())
}
#[cfg(target_os = "windows")]
pub fn install() -> Result<()> {
install_binary_windows()?;
create_registry_file()?;
add_to_windows_path()?;
add_shell_function("$HOME/.profile")?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
pub fn install() -> Result<()> {
Err(CliError::Input(
"Installation is not supported on this platform".to_string(),
))
}
fn sudo_install_binary() -> Result<()> {
let binary_path = env::current_exe()?;
Command::new("sudo")
.args(["install", "-m", "755"])
.arg(&binary_path)
.arg("/usr/local/bin/glyph")
.status()
.map_err(CliError::Io)?;
Ok(())
}
#[cfg(target_os = "linux")]
fn create_mime_type() -> Result<()> {
let mime_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-glyph">
<comment>Glyph Animation File</comment>
<glob pattern="*.glyph"/>
<magic priority="50">
<match type="string" offset="0" value="GLYF"/>
</magic>
</mime-type>
</mime-info>"#;
fs::write("/tmp/application-x-glyph.xml", mime_content)?;
Command::new("sudo")
.args([
"mv",
"/tmp/application-x-glyph.xml",
"/usr/share/mime/packages/",
])
.status()?;
Command::new("sudo")
.args(["update-mime-database", "/usr/share/mime"])
.status()?;
Ok(())
}
#[cfg(target_os = "linux")]
fn update_mailcap() -> Result<()> {
if !Command::new("grep")
.args(["-q", "application/x-glyph", "/etc/mailcap"])
.status()?
.success()
{
let entry = "application/x-glyph; glyph %s; copiousoutput\n";
let mut child = Command::new("sudo")
.args(["tee", "-a", "/etc/mailcap"])
.stdout(Stdio::null())
.stdin(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(entry.as_bytes())?;
}
child.wait()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn setup_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([
"write",
"com.apple.LaunchServices",
"LSHandlers",
"-array-add",
r#"{'LSHandlerContentType'='public.glyph';'LSHandlerRoleAll'='com.yourcompany.glyph';}"#,
])
.status()?;
}
Ok(())
}
#[cfg(target_os = "macos")]
fn add_shell_function_macos(rc_file: &str) -> Result<()> {
let rc_path = PathBuf::from(rc_file.replace("$HOME", &env::var("HOME")?));
if !rc_path.exists() {
fs::write(&rc_path, "")?;
Command::new("chmod")
.args(["0644", rc_path.to_string_lossy().as_ref()])
.status()?;
}
let metadata = fs::metadata(&rc_path)?;
let mode = metadata.permissions().mode();
Command::new("chmod")
.args(["0644", rc_path.to_string_lossy().as_ref()])
.status()?;
let result = add_shell_function(&rc_path.to_string_lossy());
Command::new("chmod")
.args([
&format!("0{:o}", mode & 0o777),
rc_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() {
fs::create_dir_all(&zsh_completion_dir)?;
Command::new("chmod")
.args(["0755", zsh_completion_dir.to_string_lossy().as_ref()])
.status()?;
}
if zsh_completion_dir.exists() {
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 install_binary_windows() -> Result<()> {
let home = env::var("HOME")?;
let bin_dir = PathBuf::from(&home).join("bin");
fs::create_dir_all(&bin_dir)?;
let current_exe = env::current_exe()?;
fs::copy(current_exe, bin_dir.join("glyph.exe"))?;
Ok(())
}
#[cfg(target_os = "windows")]
fn create_registry_file() -> Result<()> {
let home = env::var("HOME")?;
let reg_content = format!(
r#"Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\.glyph]
@="GlyphFile"
[HKEY_CLASSES_ROOT\GlyphFile\shell\open\command]
@="\"{}/bin/glyph.exe\" \"%1\""
"#,
home.replace('\\', "\\\\")
);
fs::write("glyph-association.reg", reg_content)?;
println!("Please run glyph-association.reg to associate .glyph files");
Ok(())
}
#[cfg(target_os = "windows")]
fn add_to_windows_path() -> Result<()> {
let home = env::var("HOME")?;
let profile_path = PathBuf::from(&home).join(".profile");
let path_entry = format!("export PATH=$PATH:{}/bin\n", home);
let profile_content = fs::read_to_string(&profile_path).unwrap_or_default();
if !profile_content.contains(&path_entry) {
fs::write(&profile_path, profile_content + &path_entry)?;
}
Ok(())
}
fn add_shell_function(rc_file: &str) -> Result<()> {
let shell_function = r#"
function cat() {
for arg in "$@"; do
if [[ "${arg}" == *.glyph ]]; then
glyph "${arg}"
else
command cat "${arg}"
fi
done
}
"#;
let rc_path = PathBuf::from(rc_file.replace("$HOME", &env::var("HOME")?));
let rc_content = fs::read_to_string(&rc_path).unwrap_or_default();
if !rc_content.contains("function cat()") {
fs::write(&rc_path, rc_content + shell_function)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::{tempdir, TempDir};
fn create_mock_rc(dir: &TempDir, content: &str) -> PathBuf {
let rc_path = dir.path().join(".bashrc");
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();
dir.path().to_path_buf()
}
#[test]
#[cfg(target_os = "linux")]
fn test_linux_shell_function_installation() {
let temp = tempdir().unwrap();
let rc_content = "# Existing bashrc content\nexport PATH=$HOME/bin:$PATH\n";
let rc_path = create_mock_rc(&temp, rc_content);
add_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"));
assert!(new_content.contains("command cat"));
}
#[test]
#[cfg(target_os = "linux")]
fn test_linux_shell_function_idempotent() {
let temp = tempdir().unwrap();
let rc_content = "# Existing bashrc content\nfunction cat() { echo 'test'; }\n";
let rc_path = create_mock_rc(&temp, rc_content);
add_shell_function(&rc_path.to_string_lossy()).unwrap();
let new_content = fs::read_to_string(&rc_path).unwrap();
let function_count = new_content.matches("function cat()").count();
assert_eq!(function_count, 1, "Should not add duplicate cat functions");
}
#[test]
#[cfg(target_os = "linux")]
fn test_linux_mime_type_creation() {
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");
let result = fs::write(&mime_file, "test content");
assert!(result.is_ok());
let content = fs::read_to_string(&mime_file).unwrap();
assert!(content.len() > 0);
}
#[test]
#[cfg(target_os = "macos")]
fn test_macos_shell_function_installation() {
let temp = tempdir().unwrap();
let rc_content = "# Existing zshrc content\n";
let rc_path = create_mock_rc(&temp, rc_content);
add_shell_function_macos(&rc_path.to_string_lossy()).unwrap();
let new_content = fs::read_to_string(&rc_path).unwrap();
assert!(new_content.contains("function cat()"));
}
#[test]
#[cfg(target_os = "windows")]
fn test_windows_binary_installation() {
let temp = tempdir().unwrap();
let bin_dir = temp.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let mock_exe = bin_dir.join("glyph.exe");
fs::write(&mock_exe, "mock binary content").unwrap();
assert!(mock_exe.exists());
}
#[test]
#[cfg(target_os = "windows")]
fn test_windows_registry_file_creation() {
let temp = tempdir().unwrap();
let reg_file = temp.path().join("glyph-association.reg");
fs::write(®_file, "Windows Registry Editor Version 5.00").unwrap();
assert!(reg_file.exists());
let content = fs::read_to_string(®_file).unwrap();
assert!(content.contains("Windows Registry Editor"));
}
#[test]
fn test_shell_function_with_empty_rc() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join(".bashrc");
add_shell_function(&rc_path.to_string_lossy()).unwrap();
assert!(rc_path.exists());
let content = fs::read_to_string(&rc_path).unwrap();
assert!(content.contains("function cat()"));
}
#[test]
fn test_shell_function_with_nonexistent_rc() {
let temp = tempdir().unwrap();
let rc_path = temp.path().join("nonexistent.rc");
let result = add_shell_function(&rc_path.to_string_lossy());
assert!(result.is_ok());
}
}