Skip to content

Commit

Permalink
feat(restore): WIP correct state restoration: sessions, windows, panes
Browse files Browse the repository at this point in the history
Still missing: pane contents
  • Loading branch information
graelo committed Aug 18, 2022
1 parent 5f0df8a commit ad3b860
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 44 deletions.
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Global

- use `thiserror` in the library
- cleanup error types, for instance `UnexpectedOutput`
- go over functions such as windows_related_to which return copies, and make
them return references instead
- check clap config file support
Expand All @@ -13,6 +14,8 @@

## Related to restore

- if in $TMUX, replace the existing session named `0` and switch to client
else display a message `tmux attach -t last-session-name`
- add `restore --attach` to automatically attach if running from the terminal
- add `restore --override` to replace each existing session by its version from
the archive
Expand Down
59 changes: 43 additions & 16 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, iter::zip, path::Path, time::Instant};
use std::{collections::HashSet, iter::zip, path::Path};

use anyhow::Result;
use async_std::task;
Expand All @@ -17,23 +17,22 @@ const PLACEHOLDER_SESSION_NAME: &str = "[placeholder]";

/// Restore all sessions, windows & panes from the backup file.
pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview> {
let start = Instant::now();
tmux::server::start(PLACEHOLDER_SESSION_NAME).await?;
let elapsed = start.elapsed();
println!("start server: {:?}", elapsed);
let not_in_tmux = std::env::var("TMUX").is_err();

let start = Instant::now();
if not_in_tmux {
tmux::server::start(PLACEHOLDER_SESSION_NAME).await?;
}

// 1. Restore sessions, windows and panes (without their content, see 2.)
//
let metadata = v1::read_metadata(backup_filepath).await?;
let elapsed = start.elapsed();
println!("read metadata: {:?}", elapsed);

let existing_sessions_names: HashSet<_> = tmux::session::available_sessions()
.await?
.into_iter()
.map(|s| s.name)
.collect();

let start = Instant::now();
let mut handles = vec![];

for session in &metadata.sessions {
Expand All @@ -56,6 +55,8 @@ pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview>
handles.push(handle);
}

// 2. Restore pane contents.
//
let pairs: Vec<Pair> = match join_all(handles)
.await
.into_iter()
Expand All @@ -67,9 +68,26 @@ pub async fn restore<P: AsRef<Path>>(backup_filepath: P) -> Result<v1::Overview>

eprintln!("num pairs: {}", pairs.len());

tmux::server::kill_session(PLACEHOLDER_SESSION_NAME).await?; // created above by server::start()
let elapsed = start.elapsed();
println!("create sessions: {:?}", elapsed);
// 3. Set the client last and current session.
//
tmux::client::switch_client(&metadata.client.last_session_name).await?;
tmux::client::switch_client(&metadata.client.session_name).await?;

// 4. Kill the session used to start the server.
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
);
} 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(anyhow::anyhow!(message));
}

Ok(metadata.overview())
}
Expand Down Expand Up @@ -109,13 +127,12 @@ async fn restore_session(
.expect("a window should have at least one pane");
let first_pane = first_window_panes.first().unwrap();

let (new_session_id, new_window_id, new_pane_id) = tmux::session::new_session(
let (_new_session_id, new_window_id, new_pane_id) = tmux::session::new_session(
&session,
first_pane.dirpath.as_path(),
first_window.name.as_str(),
)
.await?;
eprintln!("{new_session_id}");

// 1b. Store the association between the original pane and this new pane.
pairs.push(Pair {
Expand All @@ -134,7 +151,7 @@ async fn restore_session(
}

// 1d. Set the layout
tmux::window::set_layout(&first_window.layout, new_window_id).await?;
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) {
Expand All @@ -160,7 +177,17 @@ async fn restore_session(
}

// 2d. Set the layout
tmux::window::set_layout(&window.layout, new_window_id).await?;
tmux::window::set_layout(&window.layout, &new_window_id).await?;

if 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(pairs)
Expand Down
2 changes: 2 additions & 0 deletions src/actions/save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub async fn save<P: AsRef<Path>>(backup_dirpath: P) -> Result<(PathBuf, v1::Ove
fs::write(&temp_version_filepath, v1::FORMAT_VERSION).await?;

let version = v1::FORMAT_VERSION.to_string();
let client = tmux::client::current_client().await?;
let sessions = tmux::session::available_sessions().await?;
let windows = tmux::window::available_windows().await?;
let panes = tmux::pane::available_panes().await?;
Expand All @@ -41,6 +42,7 @@ pub async fn save<P: AsRef<Path>>(backup_dirpath: P) -> Result<(PathBuf, v1::Ove

let metadata = v1::Metadata {
version,
client,
sessions,
windows,
panes,
Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
//!
//! Work in progress
//!
//! ## Caveats
//!
//! - This is a beta version
//! - Does not handle multiple clients: help is welcome if you have clear scenarios for this.
//! - Does not handle session groups: help is also welcome.
//!
//! ## License
//!
//! Licensed under either of
Expand Down
3 changes: 3 additions & 0 deletions src/management/archive/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub struct Metadata {
/// Version of the archive's format.
pub version: String,

/// Tmux client metadata.
pub client: tmux::client::Client,

/// Tmux sessions metadata.
pub sessions: Vec<tmux::session::Session>,

Expand Down
102 changes: 102 additions & 0 deletions src/tmux/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Client-level functions: for representing client state (`client_session` etc) or reporting information inside Tmux.

use std::str::FromStr;

use async_std::process::Command;
use serde::{Deserialize, Serialize};

use crate::error;

/// A Tmux client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Client {
/// The current session.
pub session_name: String,
/// The last session.
pub last_session_name: String,
}

impl FromStr for Client {
type Err = error::ParseError;

/// Parse a string containing client information into a new `Client`.
///
/// This returns a `Result<Client, ParseError>` as this call can obviously
/// fail if provided an invalid format.
///
/// The expected format of the tmux response is
///
/// ```text
/// name-of-current-session:name-of-last-session
/// ```
///
/// This status line is obtained with
///
/// ```text
/// tmux display-message -p -F "#{client_session}:#{client_last_session}"
/// ```
///
/// For definitions, look at `Pane` type and the tmux man page for
/// definitions.
fn from_str(src: &str) -> Result<Self, Self::Err> {
let items: Vec<&str> = src.split(':').collect();
if items.len() != 2 {
return Err(error::ParseError::UnexpectedOutput(src.into()));
}

let mut iter = items.iter();

// Session id must be start with '$' followed by a `u16`
let session_name = iter.next().unwrap().to_string();
let last_session_name = iter.next().unwrap().to_string();

Ok(Client {
session_name,
last_session_name,
})
}
}

/// Return the current client useful attributes.
pub async fn current_client() -> Result<Client, error::ParseError> {
let args = vec![
"display-message",
"-p",
"-F",
"#{client_session}:#{client_last_session}",
];

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

Client::from_str(buffer.trim_end())
}

/// Return a list of all `Pane` from all sessions.
///
/// # Panics
///
/// This function panics if it can't communicate with Tmux.
pub fn display_message(message: &str) {
let args = vec!["display-message", message];

std::process::Command::new("tmux")
.args(&args)
.output()
.expect("Cannot communicate with Tmux for displaying message");
}

/// Switch to session exactly named `session_name`.

pub async fn switch_client(session_name: &str) -> Result<(), error::ParseError> {
let exact_session_name = format!("={session_name}");
let args = vec!["switch-client", "-t", &exact_session_name];

Command::new("tmux")
.args(&args)
.output()
.await
.expect("Cannot communicate with Tmux for switching the client");

Ok(())
}
17 changes: 0 additions & 17 deletions src/tmux/display.rs

This file was deleted.

4 changes: 2 additions & 2 deletions src/tmux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Functions to read or manipulate Tmux

pub mod display;
pub use display::display_message;
pub mod client;
pub use client::display_message;
pub mod layout;
pub mod pane;
pub mod pane_id;
Expand Down
22 changes: 18 additions & 4 deletions src/tmux/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
//! The main use cases are running Tmux commands & parsing Tmux panes
//! information.

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

use async_std::process::Command;
use serde::{Deserialize, Serialize};

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

/// A Tmux pane.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -114,7 +115,7 @@ impl FromStr for Pane {
}

impl Pane {
/// Returns the entire Pane content as a `String`.
/// Return the entire Pane content as a `String`.
///
/// The provided `region` specifies if the visible area is captured, or the
/// entire history.
Expand Down Expand Up @@ -152,7 +153,7 @@ impl Pane {
}
}

/// Returns a list of all `Pane` from all sessions.
/// Return a list of all `Pane` from all sessions.
pub async fn available_panes() -> Result<Vec<Pane>, error::ParseError> {
let args = vec![
"list-panes",
Expand Down Expand Up @@ -208,6 +209,19 @@ pub async fn new_pane(
Ok(new_id)
}

/// Select (make active) the pane with `pane_id`.
pub async fn select_pane(pane_id: &PaneId) -> Result<(), error::ParseError> {
let args = vec!["select-pane", "-t", pane_id.as_str()];

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

if !buffer.is_empty() {
return Err(error::ParseError::UnexpectedOutput(buffer));
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::Pane;
Expand Down
3 changes: 2 additions & 1 deletion src/tmux/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ pub async fn start(initial_session_name: &str) -> Result<(), ParseError> {

/// Remove the session named `"[placeholder]"` used to keep the server alive.
pub async fn kill_session(name: &str) -> Result<(), ParseError> {
let args = vec!["kill-session", "-t", name];
let exact_name = format!("={name}");
let args = vec!["kill-session", "-t", &exact_name];

let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;
Expand Down
4 changes: 2 additions & 2 deletions src/tmux/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl FromStr for Session {
}
let mut iter = items.iter();

// SessionId must be start with '%' followed by a `u32`
// SessionId must be start with '%' followed by a `u16`
let id_str = iter.next().unwrap();
let id = SessionId::from_str(id_str)?;

Expand All @@ -69,7 +69,7 @@ impl FromStr for Session {
}
}

/// Returns a list of all `Session` from the current tmux session.
/// Return a list of all `Session` from the current tmux session.
pub async fn available_sessions() -> Result<Vec<Session>, ParseError> {
let args = vec![
"list-sessions",
Expand Down
Loading

0 comments on commit ad3b860

Please sign in to comment.