Skip to content

Commit

Permalink
feat(restore): can restore sessions & windows
Browse files Browse the repository at this point in the history
  • Loading branch information
graelo committed Aug 18, 2022
1 parent fdf1086 commit b282c63
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 13 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
- use `thiserror` in the library
- restore windows
- restore panes
- add `restore --attach` to automatically attach if running from the terminal
39 changes: 33 additions & 6 deletions src/actions/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ use anyhow::Result;
use async_std::task;
use futures::future::join_all;

use crate::{management::archive::v1, tmux};
use crate::{
error::ParseError,
management::archive::v1,
tmux::{self, session::Session, window::Window},
};

pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview> {
tmux::server::start().await?;
Expand All @@ -21,17 +25,40 @@ pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview>
.map(|s| s.name)
.collect();

let mut restore_session_tasks = vec![];
let mut handles = vec![];

for session in &metadata.sessions {
if existing_sessions_names.contains(&session.name) {
println!("skip creating existing session {}", session.name);
eprintln!("skip creating existing session {}", session.name);
continue;
}

let session = session.clone();
let handle = task::spawn(async move { tmux::session::new_session(&session).await });
restore_session_tasks.push(handle);
let related_windows: Vec<_> = metadata.windows_related_to(&session);

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

join_all(handles).await;

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

Ok(metadata.overview())
}

/// Create the session and its windows.
///
/// The name of the session's first window is taken from the first `Window`. 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> {
// 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?;

for window in windows.iter().skip(1) {
tmux::window::new_window(window, session.dirpath.as_path(), &session.name).await?;
}

Ok(())
}
13 changes: 13 additions & 0 deletions src/management/archive/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub struct Metadata {
}

impl Metadata {
/// Return an overview of the metadata.
pub fn overview(&self) -> Overview {
Overview {
version: self.version.clone(),
Expand All @@ -53,6 +54,18 @@ impl Metadata {
num_panes: self.panes.len() as u16,
}
}

/// Return the list of windows in the provided session.
pub fn windows_related_to(
&self,
session: &tmux::session::Session,
) -> Vec<tmux::window::Window> {
self.windows
.iter()
.filter(|&w| w.sessions.contains(&session.name))
.cloned()
.collect()
}
}

/// Overview of the archive's content: number of sessions, windows and panes in the archive.
Expand Down
20 changes: 18 additions & 2 deletions src/tmux/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ use async_std::process::Command;

use crate::error::ParseError;

/// Start the Tmux server if needed, creating the `"[placeholder]"` session to keep the server
/// Name of the placeholder session.
const PLACEHOLDER_SESSION_NAME: &str = "[placeholder]";

/// Start the Tmux server if needed, creating a session named `"[placeholder]"` in order to keep the server
/// running.
///
/// It is ok-ish to already have an existing session named `"[placeholder]"`.
pub async fn start() -> Result<(), ParseError> {
let args = vec!["new-session", "-d", "-s", "[placeholder]"];
let args = vec!["new-session", "-d", "-s", PLACEHOLDER_SESSION_NAME];

let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;
Expand All @@ -19,3 +22,16 @@ pub async fn start() -> Result<(), ParseError> {
}
Err(ParseError::UnexpectedOutput(buffer))
}

/// Remove the session named `"[placeholder]"` used to keep the server alive.
pub async fn kill_placeholder_session() -> Result<(), ParseError> {
let args = vec!["kill-session", "-t", PLACEHOLDER_SESSION_NAME];

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

if buffer.is_empty() {
return Ok(());
}
Err(ParseError::UnexpectedOutput(buffer))
}
4 changes: 3 additions & 1 deletion src/tmux/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,16 @@ pub async fn available_sessions() -> Result<Vec<Session>, ParseError> {
}

/// Create a Tmux session from a `Session` struct.
pub async fn new_session(session: &Session) -> Result<(), ParseError> {
pub async fn new_session(session: &Session, window_name: &str) -> Result<(), ParseError> {
let args = vec![
"new-session",
"-d",
"-c",
session.dirpath.to_str().unwrap(),
"-s",
&session.name,
"-n",
window_name,
];

let output = Command::new("tmux").args(&args).output().await?;
Expand Down
36 changes: 33 additions & 3 deletions src/tmux/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
//! The main use cases are running Tmux commands & parsing Tmux window
//! information.

use async_std::process::Command;
use std::path::Path;
use std::str::FromStr;

use async_std::process::Command;

use serde::{Deserialize, Serialize};

use super::window_id::WindowId;
use crate::error::ParseError;

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Window {
/// Window identifier, e.g. `@3`.
pub id: WindowId,
/// Index of the Window.
/// Index of the Window in the Session.
pub index: u16,
/// Describes whether the Window is active.
pub is_active: bool,
Expand Down Expand Up @@ -120,6 +122,34 @@ pub async fn available_windows() -> Result<Vec<Window>, ParseError> {
result
}

/// Create a Tmux window from a `Window` struct, with `working_dirpath`, in session named
/// `session_name`.
pub async fn new_window(
window: &Window,
working_dirpath: &Path,
session_name: &str,
) -> Result<(), ParseError> {
let args = vec![
"new-window",
"-d",
"-c",
working_dirpath.to_str().unwrap(),
"-n",
&window.name,
"-t",
session_name,
];

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));
}

Ok(())
}

#[cfg(test)]
mod tests {
use super::Window;
Expand Down
2 changes: 1 addition & 1 deletion src/tmux/window_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};

use crate::error;

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

impl FromStr for WindowId {
Expand Down

0 comments on commit b282c63

Please sign in to comment.