use std::fs;
use std::io::Write;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::app::App;
use crate::layout::LayoutTree;
const SNAPSHOT_VERSION: u32 = 1;
#[derive(Serialize, Deserialize)]
pub struct SessionSnapshot {
pub version: u32,
pub active_ws: usize,
pub workspaces: Vec<WsSnap>,
}
#[derive(Serialize, Deserialize)]
pub struct WsSnap {
pub name: String,
pub cwd: PathBuf,
pub active_tab: usize,
pub tabs: Vec<TabSnap>,
}
#[derive(Serialize, Deserialize)]
pub struct TabSnap {
pub tree: LayoutTree,
pub focus: u32,
pub panes: Vec<(u32, PaneSnap)>,
#[serde(default)]
pub git: bool,
}
#[derive(Serialize, Deserialize)]
pub struct PaneSnap {
pub cwd: PathBuf,
pub command: String,
#[serde(default)]
pub agent_session: Option<(String, String)>,
#[serde(default)]
pub screen: Option<String>,
#[serde(default)]
pub module: Option<(String, String)>,
}
#[cfg(test)]
pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) struct TestEnv {
_guard: std::sync::MutexGuard<'static, ()>,
prev: Option<std::ffi::OsString>,
dir: PathBuf,
}
#[cfg(test)]
impl Drop for TestEnv {
fn drop(&mut self) {
match &self.prev {
Some(p) => std::env::set_var("BOHAY_HOME", p),
None => std::env::remove_var("BOHAY_HOME"),
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[cfg(test)]
pub(crate) fn test_env(tag: &str) -> TestEnv {
let guard = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var_os("BOHAY_HOME");
let dir = std::env::temp_dir().join(format!("bohay-test-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::env::set_var("BOHAY_HOME", &dir);
TestEnv {
_guard: guard,
prev,
dir,
}
}
pub fn config_dir() -> PathBuf {
if let Some(p) = std::env::var_os("BOHAY_HOME") {
return PathBuf::from(p);
}
let home = crate::platform::home_dir().unwrap_or_default();
let name = if cfg!(debug_assertions) {
".bohay-dev"
} else {
".bohay"
};
home.join(name)
}
fn session_path() -> PathBuf {
config_dir().join("session.json")
}
pub fn socket_path() -> PathBuf {
config_dir().join("bohay.sock")
}
pub fn client_socket_path() -> PathBuf {
config_dir().join("bohay-client.sock")
}
pub fn snapshot(app: &App) -> SessionSnapshot {
let mut workspaces = Vec::new();
for ws in &app.workspaces {
let mut tabs = Vec::new();
for tab in &ws.tabs {
if tab.is_git() {
tabs.push(TabSnap {
tree: tab.layout.to_tree(),
focus: tab.layout.focus.0,
panes: Vec::new(),
git: true,
});
continue;
}
let panes = tab
.layout
.leaves()
.into_iter()
.filter_map(|id| {
app.panes.get(&id).map(|p| {
let agent_session = app.status.get(&id).and_then(|s| {
s.agent_session
.as_ref()
.map(|a| (a.agent.clone(), a.session_id.clone()))
.or_else(|| {
crate::agent::latest_session(&s.agent, &p.cwd)
.map(|sid| (s.agent.clone(), sid))
})
});
let screen = p
.engine
.lock()
.ok()
.map(|e| e.snapshot_ansi())
.filter(|s| s.len() < 256 * 1024);
let module = app
.module_panes
.get(&id)
.map(|r| (r.module_id.clone(), r.entrypoint.clone()));
(
id.0,
PaneSnap {
cwd: p.cwd.clone(),
command: p.command.clone(),
agent_session,
screen,
module,
},
)
})
})
.collect();
tabs.push(TabSnap {
tree: tab.layout.to_tree(),
focus: tab.layout.focus.0,
panes,
git: false,
});
}
workspaces.push(WsSnap {
name: ws.name.clone(),
cwd: ws.cwd.clone(),
active_tab: ws.active_tab,
tabs,
});
}
SessionSnapshot {
version: SNAPSHOT_VERSION,
active_ws: app.active_ws,
workspaces,
}
}
pub fn save(app: &App) {
let snap = snapshot(app);
if snap.workspaces.is_empty() {
return;
}
let dir = config_dir();
if fs::create_dir_all(&dir).is_err() {
return;
}
let Ok(json) = serde_json::to_string_pretty(&snap) else {
return;
};
let path = session_path();
let tmp = path.with_extension("json.tmp");
if let Ok(mut f) = fs::File::create(&tmp) {
if f.write_all(json.as_bytes()).is_ok() && f.flush().is_ok() {
let _ = fs::rename(&tmp, &path);
}
}
}
pub fn load() -> Option<SessionSnapshot> {
let data = fs::read_to_string(session_path()).ok()?;
let snap: SessionSnapshot = serde_json::from_str(&data).ok()?;
if snap.version > SNAPSHOT_VERSION {
return None; }
Some(snap)
}