use anyhow::Result;
use chrono::Utc;
use std::path::{Path, PathBuf};
const LOCAL_SAVE_ENABLE_ENV: &str = "REMEM_SAVE_MEMORY_LOCAL_COPY";
const LOCAL_SAVE_DIR_ENV: &str = "REMEM_SAVE_MEMORY_LOCAL_DIR";
pub(super) fn local_copy_enabled_override(requested: Option<bool>) -> bool {
requested.unwrap_or_else(|| env_enabled(LOCAL_SAVE_ENABLE_ENV, true))
}
pub fn sanitize_segment(raw: &str, fallback: &str, limit: usize) -> String {
let mut out = String::with_capacity(raw.len().min(limit));
let mut last_underscore = false;
for ch in raw.chars() {
let mapped = if ch.is_ascii_alphanumeric() || ch == '_' {
ch.to_ascii_lowercase()
} else {
'_'
};
if mapped == '_' {
if last_underscore {
continue;
}
last_underscore = true;
} else {
last_underscore = false;
}
out.push(mapped);
if out.len() >= limit {
break;
}
}
let trimmed = out.trim_matches('_');
if trimmed.is_empty() {
fallback.to_string()
} else {
trimmed.to_string()
}
}
pub fn resolve_local_note_path(
project: &str,
title: Option<&str>,
local_path: Option<&str>,
) -> PathBuf {
if let Some(raw) = local_path.and_then(non_empty_trimmed) {
let path = PathBuf::from(raw);
if path.is_absolute() {
path
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(path)
}
} else {
default_local_note_path(project, title)
}
}
pub(super) fn build_local_note_content(project: &str, title: &str, text: &str) -> String {
let now = Utc::now().to_rfc3339();
format!(
"---\nsource: remem.save_memory\nsaved_at: {}\nproject: {}\n---\n\n# {}\n\n{}\n",
now, project, title, text
)
}
pub(super) fn write_local_note(path: &Path, content: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, content)?;
Ok(())
}
fn env_enabled(key: &str, default: bool) -> bool {
match std::env::var(key) {
Ok(value) => {
let lower = value.trim().to_ascii_lowercase();
!matches!(lower.as_str(), "0" | "false" | "no" | "off")
}
Err(_) => default,
}
}
fn remem_data_dir() -> PathBuf {
crate::db::data_dir()
}
fn default_local_note_path(project: &str, title: Option<&str>) -> PathBuf {
let base = std::env::var(LOCAL_SAVE_DIR_ENV)
.map(PathBuf::from)
.unwrap_or_else(|_| remem_data_dir().join("manual-notes"));
let project_dir = sanitize_segment(project, "manual", 64);
let title_slug = sanitize_segment(title.unwrap_or("memory"), "memory", 64);
let timestamp = chrono::Local::now().format("%Y%m%d-%H%M%S");
base.join(project_dir)
.join(format!("{}-{}.md", timestamp, title_slug))
}
fn non_empty_trimmed(raw: &str) -> Option<&str> {
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}