use std::{
collections::HashSet,
iter::zip,
path::{Path, PathBuf},
};
use futures::future::join_all;
use smol;
use tempfile::TempDir;
use crate::{
Result,
error::Error,
management::archive::v1,
tmux::{self, pane::Pane, session::Session, window::Window},
};
const PLACEHOLDER_SESSION_NAME: &str = "[placeholder]";
fn is_inside_tmux() -> bool {
std::env::var("TMUX").is_ok()
}
pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview> {
let temp_dir = TempDir::new()?;
v1::unpack(backup_filepath.as_ref(), temp_dir.path()).await?;
let panes_content_dir = temp_dir.path().join("panes-content");
let not_in_tmux = !is_inside_tmux();
if not_in_tmux {
tmux::server::start(PLACEHOLDER_SESSION_NAME).await?;
}
let default_command = tmux::server::default_command().await?;
let metadata = v1::Metadata::read_file(backup_filepath).await?;
let existing_sessions_names: HashSet<_> = tmux::session::available_sessions()
.await?
.into_iter()
.map(|s| s.name)
.collect();
let mut handles = vec![];
for session in &metadata.sessions {
if existing_sessions_names.contains(&session.name) {
eprintln!("skip creating existing session {}", session.name);
continue;
}
let session = session.clone();
let related_windows = metadata.windows_related_to(&session);
let related_panes: Vec<Vec<Pane>> = related_windows
.iter()
.map(|w| metadata.panes_related_to(w).into_iter().cloned().collect())
.collect();
let panes_content_dirpath = panes_content_dir.clone();
let default_command = default_command.clone();
let handle = smol::spawn(async move {
restore_session(
session,
related_windows,
related_panes,
panes_content_dirpath,
&default_command,
)
.await
});
handles.push(handle);
}
join_all(handles)
.await
.into_iter()
.collect::<Result<()>>()?;
temp_dir.close()?;
tmux::client::switch_client(&metadata.client.last_session_name).await?;
tmux::client::switch_client(&metadata.client.session_name).await?;
if not_in_tmux {
tmux::server::kill_session(PLACEHOLDER_SESSION_NAME).await?;
println!(
"Attach to your last session with `tmux attach -t {}`",
&metadata.client.session_name
);
Ok(metadata.overview())
} else {
if tmux::server::kill_session("0").await.is_err() {
let message = "
Unusual start conditions:
- you started from outside tmux but no existing session named `0` was found
- check the state of your session
";
return Err(Error::ConfigError(message.to_string()));
}
let metadata = v1::Metadata::new().await?;
Ok(metadata.overview())
}
}
#[derive(Debug, Clone)]
struct Pair {
source: tmux::pane::Pane,
target: tmux::pane_id::PaneId,
}
async fn restore_session(
mut session: Session,
session_windows: Vec<Window>,
panes_per_window: Vec<Vec<Pane>>,
panes_content_dir: PathBuf,
default_command: &str,
) -> Result<()> {
let mut pairs: Vec<Pair> = vec![];
for (index, (src_window, src_panes)) in zip(&session_windows, &panes_per_window).enumerate() {
let first_pane = src_panes.first().unwrap(); let content_filepath = panes_content_dir.join(format!("pane-{}.txt", first_pane.id));
let pane_command = format!(
"cat {} ; exec {}",
content_filepath.to_string_lossy(),
&default_command
);
let (new_window_id, new_pane_id) = {
if index == 0 {
let (new_session_id, new_window_id, new_pane_id) = tmux::session::new_session(
&session,
src_window,
first_pane,
Some(&pane_command),
)
.await?;
session.id = new_session_id;
(new_window_id, new_pane_id)
} else {
tmux::window::new_window(&session, src_window, first_pane, Some(&pane_command))
.await?
}
};
pairs.push(Pair {
source: first_pane.clone(),
target: new_pane_id,
});
for pane in src_panes.iter().skip(1) {
let content_filepath = panes_content_dir.join(format!("pane-{}.txt", pane.id));
let pane_command = format!(
"cat {} ; exec {}",
content_filepath.to_string_lossy(),
&default_command
);
let new_pane_id =
tmux::pane::new_pane(pane, Some(&pane_command), &new_window_id).await?;
pairs.push(Pair {
source: pane.clone(),
target: new_pane_id,
});
}
tmux::window::set_layout(&src_window.layout, &new_window_id).await?;
if src_window.is_active {
tmux::window::select_window(&new_window_id).await?;
}
}
for pair in &pairs {
if pair.source.is_active {
tmux::pane::select_pane(&pair.target).await?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
mod constants {
use super::*;
#[test]
fn placeholder_session_name_is_bracketed() {
assert!(PLACEHOLDER_SESSION_NAME.starts_with('['));
assert!(PLACEHOLDER_SESSION_NAME.ends_with(']'));
}
#[test]
fn placeholder_session_name_is_not_empty() {
assert!(PLACEHOLDER_SESSION_NAME.len() > 2);
}
}
mod tmux_detection {
use super::*;
#[test]
fn is_inside_tmux_reflects_environment() {
let expected = std::env::var("TMUX").is_ok();
assert_eq!(is_inside_tmux(), expected);
}
}
mod pair_struct {
use super::*;
use std::path::PathBuf;
use std::str::FromStr;
use tmux::pane_id::PaneId;
fn make_test_pane(id: &str, command: &str, is_active: bool) -> Pane {
Pane {
id: PaneId::from_str(id).unwrap(),
index: 0,
is_active,
title: "test".to_string(),
dirpath: PathBuf::from("/tmp"),
command: command.to_string(),
}
}
#[test]
fn pair_can_be_cloned() {
let pane = make_test_pane("%1", "zsh", true);
let pair = Pair {
source: pane.clone(),
target: PaneId::from_str("%2").unwrap(),
};
let cloned = pair.clone();
assert_eq!(cloned.source.id, pair.source.id);
assert_eq!(cloned.target, pair.target);
}
#[test]
fn pair_is_debug_printable() {
let pane = make_test_pane("%1", "bash", false);
let pair = Pair {
source: pane,
target: PaneId::from_str("%5").unwrap(),
};
let debug_str = format!("{pair:?}");
assert!(debug_str.contains("Pair"));
}
#[test]
fn pair_preserves_source_pane_properties() {
let pane = make_test_pane("%42", "nvim", true);
let pair = Pair {
source: pane,
target: PaneId::from_str("%99").unwrap(),
};
assert_eq!(pair.source.command, "nvim");
assert!(pair.source.is_active);
assert_eq!(pair.target.as_str(), "%99");
}
}
}