use crate::errors::CoreError;
use chrono::Utc;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
pub fn backup_file(path: &str, backup_dir: &Path) -> Result<(), CoreError> {
if !Path::new(path).exists() {
return Ok(());
}
let sanitized = path
.replace(['/', '\\'], "_")
.trim_start_matches('_')
.to_string();
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let backup_path = backup_dir.join(format!("{sanitized}.{timestamp}.bak"));
std::fs::copy(path, &backup_path).map_err(|e| {
CoreError::Io(format!(
"backing up {} to {}: {e}",
path,
backup_path.display()
))
})?;
Ok(())
}
pub fn truncate_str(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut i = max;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
&s[..i]
}
pub fn shorten_path(path: &str) -> String {
if let Some(home) = std::env::var_os("HOME") {
let home_str = home.to_string_lossy();
if path.starts_with(home_str.as_ref()) {
return format!("~{}", &path[home_str.len()..]);
}
}
path.to_string()
}
pub fn shorten_path_buf(path: &std::path::Path) -> String {
shorten_path(&path.display().to_string())
}
pub fn log_parse_warning(msg: &str) {
let log_path = crate::config::retro_dir().join("warnings.log");
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
let ts = Utc::now().format("%Y-%m-%dT%H:%M:%S");
let _ = writeln!(file, "[{ts}] {msg}");
}
}
pub fn strip_code_fences(content: &str) -> String {
let trimmed = content.trim();
if !trimmed.starts_with("```") {
return trimmed.to_string();
}
let lines: Vec<&str> = trimmed.lines().collect();
let mut result = Vec::new();
let mut in_block = false;
for line in lines {
if line.starts_with("```") && !in_block {
in_block = true;
continue;
}
if line.starts_with("```") && in_block {
break;
}
if in_block {
result.push(line);
}
}
if result.is_empty() {
trimmed.to_string()
} else {
result.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_json_fences() {
let input = "```json\n{\"key\": \"value\"}\n```";
assert_eq!(strip_code_fences(input), "{\"key\": \"value\"}");
}
#[test]
fn test_strip_yaml_fences() {
let input = "```yaml\n---\nname: test\n---\nbody\n```";
assert_eq!(strip_code_fences(input), "---\nname: test\n---\nbody");
}
#[test]
fn test_strip_bare_fences() {
let input = "```\ncontent here\n```";
assert_eq!(strip_code_fences(input), "content here");
}
#[test]
fn test_no_fences() {
let input = "just plain text";
assert_eq!(strip_code_fences(input), "just plain text");
}
#[test]
fn test_whitespace_trimmed() {
let input = " \n```json\n{}\n```\n ";
assert_eq!(strip_code_fences(input), "{}");
}
#[test]
fn test_truncate_str_ascii() {
assert_eq!(truncate_str("hello world", 5), "hello");
}
#[test]
fn test_truncate_str_no_truncation() {
assert_eq!(truncate_str("short", 100), "short");
}
#[test]
fn test_truncate_str_utf8_boundary() {
let s = "caf\u{00e9}!";
assert_eq!(truncate_str(s, 4), "caf");
}
#[test]
fn test_truncate_str_empty() {
assert_eq!(truncate_str("", 10), "");
}
#[test]
fn test_shorten_path_replaces_home() {
let home = std::env::var("HOME").unwrap();
let input = format!("{home}/projects/foo");
assert_eq!(shorten_path(&input), "~/projects/foo");
}
#[test]
fn test_shorten_path_no_home_prefix() {
assert_eq!(shorten_path("/tmp/foo"), "/tmp/foo");
}
#[test]
fn test_shorten_path_buf_works() {
let home = std::env::var("HOME").unwrap();
let p = std::path::PathBuf::from(format!("{home}/.retro/retro.db"));
assert_eq!(shorten_path_buf(&p), "~/.retro/retro.db");
}
}