use crate::error::{AppError, Result};
use std::env;
use std::fs;
use std::path::PathBuf;
const MARKER_START: &str = "# TR-300 Machine Report";
const MARKER_END: &str = "# End TR-300";
const SHELL_ADDITIONS: &str = r#"# TR-300 Machine Report
alias report='tr300'
# Auto-run on interactive shell
case "$-" in *i*)
tr300 --fast
;; esac
# End TR-300"#;
pub fn install_path() -> PathBuf {
if let Some(home) = dirs::home_dir() {
let local_bin = home.join(".local").join("bin");
if local_bin.exists() {
return local_bin.join("tr300");
}
}
PathBuf::from("/usr/local/bin/tr300")
}
pub fn install() -> Result<()> {
let home =
dirs::home_dir().ok_or_else(|| AppError::platform("Could not determine home directory"))?;
let mut modified_files = Vec::new();
let bashrc = home.join(".bashrc");
if bashrc.exists() && update_shell_profile(&bashrc)? {
modified_files.push(bashrc.display().to_string());
}
let zshrc = home.join(".zshrc");
if zshrc.exists() && update_shell_profile(&zshrc)? {
modified_files.push(zshrc.display().to_string());
}
if modified_files.is_empty() && !bashrc.exists() {
fs::write(&bashrc, SHELL_ADDITIONS).map_err(|e| {
AppError::platform(format!("Failed to create {}: {}", bashrc.display(), e))
})?;
modified_files.push(bashrc.display().to_string());
}
if modified_files.is_empty() {
return Err(AppError::platform("No shell profile found to update"));
}
println!("Modified shell profiles:");
for file in &modified_files {
println!(" - {}", file);
}
Ok(())
}
pub fn uninstall() -> Result<()> {
let home =
dirs::home_dir().ok_or_else(|| AppError::platform("Could not determine home directory"))?;
let mut modified_files = Vec::new();
let bashrc = home.join(".bashrc");
if bashrc.exists() && remove_from_profile(&bashrc)? {
modified_files.push(bashrc.display().to_string());
}
let zshrc = home.join(".zshrc");
if zshrc.exists() && remove_from_profile(&zshrc)? {
modified_files.push(zshrc.display().to_string());
}
if modified_files.is_empty() {
println!("No TR-300 configuration found in shell profiles.");
} else {
println!("Cleaned shell profiles:");
for file in &modified_files {
println!(" - {}", file);
}
}
Ok(())
}
fn update_shell_profile(path: &PathBuf) -> Result<bool> {
let content = fs::read_to_string(path)
.map_err(|e| AppError::platform(format!("Failed to read {}: {}", path.display(), e)))?;
let cleaned_content = remove_tr300_block(&content);
let new_content = if cleaned_content.trim().is_empty() {
format!("{}\n", SHELL_ADDITIONS)
} else {
format!("{}\n\n{}\n", cleaned_content.trim_end(), SHELL_ADDITIONS)
};
fs::write(path, &new_content)
.map_err(|e| AppError::platform(format!("Failed to write {}: {}", path.display(), e)))?;
Ok(true)
}
fn remove_tr300_block(content: &str) -> String {
let mut lines: Vec<&str> = content.lines().collect();
lines = remove_delimited_block(&lines, MARKER_START, MARKER_END);
let mut result = Vec::new();
let mut prev_blank = false;
for line in lines {
let is_blank = line.trim().is_empty();
if is_blank && prev_blank {
continue;
}
result.push(line);
prev_blank = is_blank;
}
while result.last().map(|s| s.trim().is_empty()).unwrap_or(false) {
result.pop();
}
if result.is_empty() {
String::new()
} else {
result.join("\n") + "\n"
}
}
fn remove_delimited_block<'a>(lines: &[&'a str], start: &str, end: &str) -> Vec<&'a str> {
let mut result = Vec::new();
let mut in_block = false;
for line in lines {
if line.contains(start) {
in_block = true;
continue;
}
if line.contains(end) {
in_block = false;
continue;
}
if !in_block {
result.push(*line);
}
}
result
}
fn remove_from_profile(path: &PathBuf) -> Result<bool> {
let content = fs::read_to_string(path)
.map_err(|e| AppError::platform(format!("Failed to read {}: {}", path.display(), e)))?;
if !content.contains(MARKER_START) {
return Ok(false);
}
let mut new_lines = Vec::new();
let mut in_tr300_block = false;
for line in content.lines() {
if line.contains(MARKER_START) {
in_tr300_block = true;
continue;
}
if line.contains(MARKER_END) {
in_tr300_block = false;
continue;
}
if !in_tr300_block {
new_lines.push(line);
}
}
while new_lines.last().map(|s| s.is_empty()).unwrap_or(false) {
new_lines.pop();
}
let new_content = new_lines.join("\n") + "\n";
fs::write(path, new_content)
.map_err(|e| AppError::platform(format!("Failed to write {}: {}", path.display(), e)))?;
Ok(true)
}
pub fn find_binary_location() -> Option<PathBuf> {
if let Ok(exe_path) = env::current_exe() {
if exe_path.exists() {
return Some(exe_path);
}
}
let path = install_path();
if path.exists() {
return Some(path);
}
None
}
pub fn remove_binary(binary_path: &PathBuf) -> Result<()> {
if !binary_path.exists() {
return Ok(());
}
fs::remove_file(binary_path).map_err(|e| {
AppError::platform(format!(
"Failed to remove binary {}: {}",
binary_path.display(),
e
))
})?;
println!("Removed binary: {}", binary_path.display());
Ok(())
}
pub fn uninstall_complete() -> Result<()> {
uninstall()?;
if let Some(binary_path) = find_binary_location() {
remove_binary(&binary_path)?;
}
Ok(())
}