pub const APP_NAME: &str = "repartee";
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const APP_URL: &str = "https://repart.ee/";
pub const DICTS_REPO_URL: &str =
"https://raw.githubusercontent.com/outragedevs/repartee-dicts/main";
pub const DICTS_MANIFEST_URL: &str =
"https://raw.githubusercontent.com/outragedevs/repartee-dicts/main/manifest.json";
pub fn default_quit_message() -> String {
format!("{APP_NAME} {APP_VERSION} — {APP_URL}")
}
pub const WHOX_FIELDS: &str = "%tcuihnfar";
const BUNDLED_THEMES: &[(&str, &str)] = &[
("default.theme", include_str!("../themes/default.theme")),
("spring.theme", include_str!("../themes/spring.theme")),
];
const DEFAULT_THEME_FILE: &str = "default.theme";
use std::path::{Path, PathBuf};
pub fn home_dir() -> PathBuf {
let base = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
base.join(format!(".{APP_NAME}"))
}
pub fn config_path() -> PathBuf {
home_dir().join("config.toml")
}
pub fn theme_dir() -> PathBuf {
home_dir().join("themes")
}
pub fn env_path() -> PathBuf {
home_dir().join(".env")
}
pub fn log_dir() -> PathBuf {
home_dir().join("logs")
}
pub fn scripts_dir() -> PathBuf {
home_dir().join("scripts")
}
pub fn sessions_dir() -> PathBuf {
home_dir().join("sessions")
}
pub fn dicts_dir() -> PathBuf {
home_dir().join("dicts")
}
pub fn certs_dir() -> PathBuf {
home_dir().join("certs")
}
pub fn ensure_config_dir() {
if let Err(e) = crate::fs_secure::create_dir_all(&home_dir(), 0o700) {
tracing::warn!("failed to create app dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&theme_dir(), 0o700) {
tracing::warn!("failed to create theme dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&log_dir(), 0o700) {
tracing::warn!("failed to create log dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&scripts_dir(), 0o700) {
tracing::warn!("failed to create scripts dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&sessions_dir(), 0o700) {
tracing::warn!("failed to create sessions dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&dicts_dir(), 0o700) {
tracing::warn!("failed to create dicts dir: {e}");
}
if let Err(e) = crate::fs_secure::create_dir_all(&certs_dir(), 0o700) {
tracing::warn!("failed to create certs dir: {e}");
}
let cfg = config_path();
if !cfg.exists() {
let default_cfg = crate::config::default_config();
if let Err(e) = crate::config::save_config(&cfg, &default_cfg) {
tracing::warn!("failed to write default config: {e}");
} else {
tracing::info!("Created default config at {}", cfg.display());
}
} else if let Err(e) = crate::fs_secure::restrict_path(&cfg, 0o600) {
tracing::warn!("failed to secure config file: {e}");
}
let env = env_path();
if env.exists()
&& let Err(e) = crate::fs_secure::restrict_path(&env, 0o600)
{
tracing::warn!("failed to secure env file: {e}");
}
sync_bundled_themes_in(&theme_dir());
}
fn sync_bundled_themes_in(dir: &Path) {
for (name, content) in BUNDLED_THEMES {
let path = dir.join(name);
if !path.exists() {
match std::fs::write(&path, content) {
Ok(()) => tracing::info!("Installed bundled theme: {}", path.display()),
Err(e) => tracing::warn!("failed to write bundled theme {name}: {e}"),
}
continue;
}
if *name != DEFAULT_THEME_FILE {
continue;
}
let Ok(current) = std::fs::read_to_string(&path) else {
tracing::warn!("failed to read existing {name} for diff check");
continue;
};
if current == *content {
continue;
}
let backup = dir.join(format!("{name}.bak"));
if let Err(e) = std::fs::copy(&path, &backup) {
tracing::warn!(
"failed to back up {name} to {}: {e} — skipping update",
backup.display()
);
continue;
}
match std::fs::write(&path, content) {
Ok(()) => tracing::info!(
"Updated {} (previous version backed up to {})",
path.display(),
backup.display()
),
Err(e) => tracing::warn!("failed to update {name}: {e}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bundled_default() -> &'static str {
BUNDLED_THEMES
.iter()
.find(|(name, _)| *name == DEFAULT_THEME_FILE)
.map(|(_, c)| *c)
.expect("default.theme must be in BUNDLED_THEMES")
}
#[test]
fn sync_installs_all_themes_on_first_run() {
let dir = tempfile::tempdir().unwrap();
sync_bundled_themes_in(dir.path());
for (name, content) in BUNDLED_THEMES {
let path = dir.path().join(name);
assert!(path.exists(), "{name} should have been created");
assert_eq!(std::fs::read_to_string(&path).unwrap(), *content);
}
}
#[test]
fn sync_leaves_matching_default_theme_untouched() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(DEFAULT_THEME_FILE);
std::fs::write(&path, bundled_default()).unwrap();
sync_bundled_themes_in(dir.path());
assert!(
!dir.path().join("default.theme.bak").exists(),
"no backup should be created when content matches"
);
}
#[test]
fn sync_backs_up_and_overwrites_changed_default_theme() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(DEFAULT_THEME_FILE);
let user_content = "# user's old default theme\n";
std::fs::write(&path, user_content).unwrap();
sync_bundled_themes_in(dir.path());
let backup = dir.path().join("default.theme.bak");
assert!(backup.exists(), "backup should exist after overwrite");
assert_eq!(std::fs::read_to_string(&backup).unwrap(), user_content);
assert_eq!(std::fs::read_to_string(&path).unwrap(), bundled_default());
}
#[test]
fn sync_preserves_user_customized_non_default_theme() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("spring.theme");
let user_customization = "# my customized spring theme\n";
std::fs::write(&path, user_customization).unwrap();
sync_bundled_themes_in(dir.path());
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
user_customization,
"non-default themes must never be overwritten"
);
assert!(
!dir.path().join("spring.theme.bak").exists(),
"non-default themes must not produce .bak files"
);
}
}