use super::{ArrangementManager, WindowArrangement};
use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn arrangements_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("par-term")
.join("arrangements.yaml")
}
pub fn load_arrangements() -> Result<ArrangementManager> {
load_arrangements_from(arrangements_path())
}
pub fn load_arrangements_from(path: PathBuf) -> Result<ArrangementManager> {
crate::debug_info!("ARRANGEMENT", "Loading arrangements from {:?}", path);
if !path.exists() {
crate::debug_info!(
"ARRANGEMENT",
"No arrangements file found at {:?}, starting with empty arrangements",
path
);
return Ok(ArrangementManager::new());
}
let contents = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read arrangements from {:?}", path))?;
crate::debug_info!(
"ARRANGEMENT",
"Read {} bytes from arrangements file",
contents.len()
);
if contents.trim().is_empty() {
crate::debug_info!(
"ARRANGEMENT",
"Arrangements file is empty, starting with empty arrangements"
);
return Ok(ArrangementManager::new());
}
let arrangements: Vec<WindowArrangement> = serde_yaml_ng::from_str(&contents)
.with_context(|| format!("Failed to parse arrangements from {:?}", path))?;
crate::debug_info!(
"ARRANGEMENT",
"Parsed {} arrangements from {:?}",
arrangements.len(),
path
);
for a in &arrangements {
crate::debug_info!(
"ARRANGEMENT",
" - {}: {} ({} windows)",
a.id,
a.name,
a.windows.len()
);
}
Ok(ArrangementManager::from_arrangements(arrangements))
}
pub fn save_arrangements(manager: &ArrangementManager) -> Result<()> {
save_arrangements_to(manager, arrangements_path())
}
pub fn save_arrangements_to(manager: &ArrangementManager, path: PathBuf) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory {:?}", parent))?;
}
let arrangements = manager.to_vec();
let contents =
serde_yaml_ng::to_string(&arrangements).context("Failed to serialize arrangements")?;
std::fs::write(&path, contents)
.with_context(|| format!("Failed to write arrangements to {:?}", path))?;
log::info!("Saved {} arrangements to {:?}", arrangements.len(), path);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::arrangements::{MonitorInfo, TabSnapshot, WindowArrangement, WindowSnapshot};
use tempfile::tempdir;
use uuid::Uuid;
#[test]
fn test_load_nonexistent_file() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp.path().join("nonexistent.yaml");
let manager = load_arrangements_from(path)
.expect("loading from nonexistent path should return empty manager");
assert!(manager.is_empty());
}
#[test]
fn test_load_empty_file() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp.path().join("empty.yaml");
std::fs::write(&path, "").expect("failed to write empty file");
let manager =
load_arrangements_from(path).expect("loading empty file should return empty manager");
assert!(manager.is_empty());
}
#[test]
fn test_save_and_load_roundtrip() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp.path().join("arrangements.yaml");
let mut manager = ArrangementManager::new();
manager.add(WindowArrangement {
id: Uuid::new_v4(),
name: "Work Setup".to_string(),
monitor_layout: vec![MonitorInfo {
name: Some("Main".to_string()),
index: 0,
position: (0, 0),
size: (1920, 1080),
scale_factor: 1.0,
}],
windows: vec![WindowSnapshot {
monitor: MonitorInfo {
name: Some("Main".to_string()),
index: 0,
position: (0, 0),
size: (1920, 1080),
scale_factor: 1.0,
},
position_relative: (100, 100),
size: (800, 600),
tabs: vec![TabSnapshot {
cwd: Some("/home/user/work".to_string()),
title: "work".to_string(),
custom_color: None,
user_title: None,
custom_icon: None,
}],
active_tab_index: 0,
tmux_session_name: None,
}],
created_at: "2024-01-01T00:00:00Z".to_string(),
order: 0,
});
save_arrangements_to(&manager, path.clone()).expect("failed to save arrangements");
let loaded = load_arrangements_from(path).expect("failed to load saved arrangements");
assert_eq!(loaded.len(), 1);
let arrangements = loaded.arrangements_ordered();
assert_eq!(arrangements[0].name, "Work Setup");
assert_eq!(arrangements[0].windows.len(), 1);
assert_eq!(arrangements[0].windows[0].tabs.len(), 1);
assert_eq!(
arrangements[0].windows[0].tabs[0].cwd,
Some("/home/user/work".to_string())
);
}
#[test]
fn test_roundtrip_preserves_custom_tab_properties() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp.path().join("arrangements.yaml");
let mut manager = ArrangementManager::new();
manager.add(WindowArrangement {
id: Uuid::new_v4(),
name: "Custom Props".to_string(),
monitor_layout: vec![MonitorInfo {
name: Some("Main".to_string()),
index: 0,
position: (0, 0),
size: (1920, 1080),
scale_factor: 1.0,
}],
windows: vec![WindowSnapshot {
monitor: MonitorInfo {
name: Some("Main".to_string()),
index: 0,
position: (0, 0),
size: (1920, 1080),
scale_factor: 1.0,
},
position_relative: (0, 0),
size: (800, 600),
tabs: vec![
TabSnapshot {
cwd: Some("/home/user".to_string()),
title: "My Custom Tab".to_string(),
custom_color: Some([255, 128, 0]),
user_title: Some("My Custom Tab".to_string()),
custom_icon: Some("🔥".to_string()),
},
TabSnapshot {
cwd: Some("/tmp".to_string()),
title: "Tab 2".to_string(),
custom_color: None,
user_title: None,
custom_icon: Some("📁".to_string()),
},
TabSnapshot {
cwd: None,
title: "Colored Only".to_string(),
custom_color: Some([0, 200, 100]),
user_title: None,
custom_icon: None,
},
],
active_tab_index: 1,
tmux_session_name: None,
}],
created_at: "2024-01-01T00:00:00Z".to_string(),
order: 0,
});
save_arrangements_to(&manager, path.clone())
.expect("failed to save custom-props arrangements");
let loaded =
load_arrangements_from(path).expect("failed to load custom-props arrangements");
let arrangements = loaded.arrangements_ordered();
let tabs = &arrangements[0].windows[0].tabs;
assert_eq!(tabs[0].custom_color, Some([255, 128, 0]));
assert_eq!(tabs[0].user_title, Some("My Custom Tab".to_string()));
assert_eq!(tabs[0].custom_icon, Some("🔥".to_string()));
assert_eq!(tabs[1].custom_color, None);
assert_eq!(tabs[1].user_title, None);
assert_eq!(tabs[1].custom_icon, Some("📁".to_string()));
assert_eq!(tabs[2].custom_color, Some([0, 200, 100]));
assert_eq!(tabs[2].user_title, None);
assert_eq!(tabs[2].custom_icon, None);
}
#[test]
fn test_save_creates_parent_directory() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp
.path()
.join("nested")
.join("dir")
.join("arrangements.yaml");
let manager = ArrangementManager::new();
save_arrangements_to(&manager, path.clone())
.expect("failed to save arrangements to nested dir");
assert!(path.exists());
}
#[test]
fn test_load_corrupt_file_returns_error() {
let temp = tempdir().expect("failed to create temp dir");
let path = temp.path().join("corrupt.yaml");
std::fs::write(&path, "not: valid: yaml: [[[").expect("failed to write corrupt file");
let result = load_arrangements_from(path);
assert!(result.is_err());
}
}