Skip to content

Commit

Permalink
feat(restore): WIP restore windows, panes & layout
Browse files Browse the repository at this point in the history
  • Loading branch information
graelo committed Aug 18, 2022
1 parent 1bfd724 commit 01b7755
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 46 deletions.
112 changes: 95 additions & 17 deletions src/actions/restore.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Restore sessions, windows and panes from the content of a backup.

use std::{collections::HashSet, path::Path};
use std::{collections::HashSet, iter::zip, path::Path};

use anyhow::Result;
use async_std::task;
Expand All @@ -9,14 +9,12 @@ use futures::future::join_all;
use crate::{
error::ParseError,
management::archive::v1,
tmux::{self, session::Session, window::Window},
tmux::{self, pane::Pane, session::Session, window::Window},
};

pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview> {
tmux::server::start().await?;

println!("restoring `{}`", backup_filepath.as_ref().to_string_lossy());

let metadata = v1::read_metadata(backup_filepath).await?;

let existing_sessions_names: HashSet<_> = tmux::session::available_sessions()
Expand All @@ -35,34 +33,114 @@ pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview>

let session = session.clone();
let related_windows = metadata.windows_related_to(&session);

let handle = task::spawn(async move { restore_session(session, related_windows).await });
let related_panes: Vec<Vec<Pane>> = related_windows
.iter()
.map(|w| metadata.panes_related_to(w).into_iter().cloned().collect())
.collect();

let handle =
task::spawn(
async move { restore_session(session, related_windows, related_panes).await },
);
handles.push(handle);
}

join_all(handles).await;
if let Err(e) = join_all(handles)
.await
.into_iter()
.collect::<Result<Vec<_>, ParseError>>()
{
return Err(anyhow::anyhow!("error: {e}"));
}

tmux::server::kill_placeholder_session().await?; // created above by server::start()

Ok(metadata.overview())
}

/// Create the session along with its windows and panes.
/// Associates a pane from the backup with a new target pane id.
#[derive(Debug, Clone)]
struct Pair {
/// Pane definition from the backup.
source: tmux::pane::Pane,
/// Target pane id.
target: tmux::pane_id::PaneId,
}

/// Create a session along with its windows and panes.
///
/// The session is created with the first window in order to give it the right name. The remainder
/// of windows are created in sequence, to preserve the order from the backup.
async fn restore_session(session: Session, windows: Vec<Window>) -> Result<(), ParseError> {
// 1. Create the session and the windows (each has one empty pane).
///
async fn restore_session(
session: Session,
related_windows: Vec<Window>,
related_panes: Vec<Vec<Pane>>,
) -> Result<(), ParseError> {
// 1a. Create the session and the first window (and the first pane as a side-effect).

let first_window = related_windows
.first()
.expect("a session should have at least one window");
let first_window_panes = related_panes
.first()
.expect("a window should have at least one pane");
let first_pane = first_window_panes.first().unwrap();

let (new_window_id, new_pane_id) = tmux::session::new_session(
&session,
first_pane.dirpath.as_path(),
first_window.name.as_str(),
)
.await?;

let mut pairs: Vec<Pair> = vec![];

// 1b. Store the association between the original pane and this new pane.
pairs.push(Pair {
source: first_pane.clone(),
target: new_pane_id,
});

// 1c. Create the other panes of the first window, storing their association with the original
// panes for this first window. Each new pane is configured as the original pane.
for pane in first_window_panes.iter().skip(1) {
let new_pane_id = tmux::pane::new_pane(pane, &new_window_id).await?;
pairs.push(Pair {
source: pane.clone(),
target: new_pane_id,
});
}

// A session is guaranteed to have at least one window.
let first_window_name = windows.first().unwrap().name.as_str();
tmux::session::new_session(&session, first_window_name).await?;
// 1d. Set the layout
tmux::window::set_layout(&first_window.layout, new_window_id).await?;

// 2. Create the other windows (and their first pane as a side-effect).
for (window, panes) in zip(&related_windows, &related_panes).skip(1) {
let first_pane = panes
.first()
.expect("a window should have at least one pane");
let (new_window_id, new_pane_id) =
tmux::window::new_window(window, first_pane.dirpath.as_path(), &session.name).await?;

// 2b. Store the association between the original pane and this new pane.
pairs.push(Pair {
source: first_pane.clone(),
target: new_pane_id,
});

// 2c. Then add the new panes in each window.
for pane in panes.iter().skip(1) {
let new_pane_id = tmux::pane::new_pane(pane, &new_window_id).await?;
pairs.push(Pair {
source: pane.clone(),
target: new_pane_id,
});
}

for window in windows.iter().skip(1) {
tmux::window::new_window(window, session.dirpath.as_path(), &session.name).await?;
// 2d. Set the layout
tmux::window::set_layout(&window.layout, new_window_id).await?;
}

// 2. Create panes in each window.

Ok(())
}
10 changes: 10 additions & 0 deletions src/management/archive/v1.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Support functions to create and read backup archive files.

use std::collections::HashSet;
use std::fmt;
use std::io::Read;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -66,6 +67,15 @@ impl Metadata {
.cloned()
.collect()
}

/// Return the list of panes in the provided window.
pub fn panes_related_to(&self, window: &tmux::window::Window) -> Vec<&tmux::pane::Pane> {
let pane_ids: HashSet<tmux::pane_id::PaneId> = window.pane_ids().iter().cloned().collect();
self.panes
.iter()
.filter(|&p| pane_ids.contains(&p.id))
.collect()
}
}

/// Overview of the archive's content: number of sessions, windows and panes in the archive.
Expand Down
39 changes: 33 additions & 6 deletions src/tmux/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::path::PathBuf;
use std::str::FromStr;

use super::pane_id::PaneId;
use super::window_id::WindowId;
use crate::error;
use serde::{Deserialize, Serialize};

Expand All @@ -33,8 +34,8 @@ pub struct Pane {
pub pos_bottom: u16,
/// Title of the Pane (usually defaults to the hostname)
pub title: String,
/// Current path of the Pane
pub path: PathBuf,
/// Current dirpath of the Pane
pub dirpath: PathBuf,
/// Current command executed in the Pane
pub command: String,
}
Expand Down Expand Up @@ -105,7 +106,7 @@ impl FromStr for Pane {
pos_top,
pos_bottom,
title,
path,
dirpath: path,
command,
})
}
Expand Down Expand Up @@ -180,6 +181,32 @@ pub async fn available_panes() -> Result<Vec<Pane>, error::ParseError> {
result
}

/// Create a new pane (horizontal split) in the window with `window_id`, and return the new
/// pane id.
pub async fn new_pane(
reference_pane: &Pane,
window_id: &WindowId,
) -> Result<PaneId, error::ParseError> {
let args = vec![
"split-window",
"-h",
"-c",
reference_pane.dirpath.to_str().unwrap(),
"-t",
window_id.as_str(),
"-P",
"-F",
"#{pane_id}",
];

let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;

let new_id = PaneId::from_str(buffer.trim_end())?;

Ok(new_id)
}

#[cfg(test)]
mod tests {
use super::Pane;
Expand Down Expand Up @@ -211,7 +238,7 @@ mod tests {
pos_top: 0,
pos_bottom: 84,
title: String::from("rmbp"),
path: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("nvim"),
},
Pane {
Expand All @@ -225,7 +252,7 @@ mod tests {
pos_top: 0,
pos_bottom: 41,
title: String::from("rmbp"),
path: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("tmux"),
},
Pane {
Expand All @@ -239,7 +266,7 @@ mod tests {
pos_top: 43,
pos_bottom: 84,
title: String::from("rmbp"),
path: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("man"),
},
];
Expand Down
4 changes: 2 additions & 2 deletions src/tmux/pane_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::str::FromStr;
use crate::error;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PaneId(pub String);

impl FromStr for PaneId {
Expand All @@ -13,7 +13,7 @@ impl FromStr for PaneId {
/// Parse into PaneId. The `&str` must start with '%' followed by a `u32`.
fn from_str(src: &str) -> Result<Self, Self::Err> {
if !src.starts_with('%') {
return Err(error::ParseError::ExpectedIdMarker('$'));
return Err(error::ParseError::ExpectedIdMarker('%'));
}
let id = src[1..].parse::<u16>()?;
let id = format!("%{}", id);
Expand Down
35 changes: 26 additions & 9 deletions src/tmux/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
//! information.

use async_std::process::Command;
use std::{path::PathBuf, str::FromStr};
use std::{
path::{Path, PathBuf},
str::FromStr,
};

use serde::{Deserialize, Serialize};

use super::session_id::SessionId;
use super::{pane_id::PaneId, session_id::SessionId, window_id::WindowId};
use crate::error::ParseError;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -87,27 +90,41 @@ pub async fn available_sessions() -> Result<Vec<Session>, ParseError> {
result
}

/// Create a Tmux session from a `Session` struct.
pub async fn new_session(session: &Session, window_name: &str) -> Result<(), ParseError> {
/// Create a Tmux session from a `Session` struct, and name the window `window_name`.
pub async fn new_session(
session: &Session,
dirpath: &Path,
window_name: &str,
) -> Result<(WindowId, PaneId), ParseError> {
let args = vec![
"new-session",
"-d",
"-c",
session.dirpath.to_str().unwrap(),
dirpath.to_str().unwrap(),
"-s",
&session.name,
"-n",
window_name,
"-P",
"-F",
"#{window_id}:#{pane_id}",
];

let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;

if !buffer.is_empty() {
return Err(ParseError::UnexpectedOutput(buffer));
}
let items: Vec<&str> = buffer.trim_end().split(':').collect();
assert_eq!(items.len(), 2);

let mut iter = items.iter();

let id_str = iter.next().unwrap();
let new_window_id = WindowId::from_str(id_str)?;

let id_str = iter.next().unwrap();
let new_pane_id = PaneId::from_str(id_str)?;

Ok(())
Ok((new_window_id, new_pane_id))
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit 01b7755

Please sign in to comment.