Skip to content

Commit

Permalink
Merge pull request #18 from graelo/feat/droplines
Browse files Browse the repository at this point in the history
Add droplines
  • Loading branch information
graelo committed Oct 26, 2022
2 parents d59dc20 + 9c6656d commit 523c4dc
Show file tree
Hide file tree
Showing 20 changed files with 142 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/large-scope.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
rust: [1.59.0, beta, nightly]
rust: [1.60.0, beta, nightly]

steps:
- name: Rust install
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [
name = "tmux-backup"
version = "0.2.0"
edition = "2021"
rust-version = "1.59.0"
rust-version = "1.60.0"
description = "A backup & restore solution for Tmux sessions."
readme = "README.md"

Expand Down
4 changes: 2 additions & 2 deletions ci/test_full.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -e

CRATE=tmux-backup
MSRV=1.59
MSRV=1.60

get_rust_version() {
local array=($(rustc --version));
Expand All @@ -28,7 +28,7 @@ if ! check_version $MSRV ; then
fi

FEATURES=()
# check_version 1.59 && FEATURES+=(libm)
# check_version 1.60 && FEATURES+=(libm)
echo "Testing supported features: ${FEATURES[*]}"

set -x
Expand Down
17 changes: 13 additions & 4 deletions src/actions/save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use crate::{management::archive::v1, tmux, Result};
/// - The `backup_dirpath` folder is assumed to exist (done during catalog initialization).
/// - Backups have a name similar to `backup-20220731T222948.tar.zst`.
///
pub async fn save<P: AsRef<Path>>(backup_dirpath: P) -> Result<(PathBuf, v1::Overview)> {
pub async fn save<P: AsRef<Path>>(
backup_dirpath: P,
num_lines_to_drop: usize,
) -> Result<(PathBuf, v1::Overview)> {
// Prepare the temp directory.
let temp_dir = TempDir::new()?;

Expand Down Expand Up @@ -53,7 +56,7 @@ pub async fn save<P: AsRef<Path>>(backup_dirpath: P) -> Result<(PathBuf, v1::Ove

let panes = tmux::pane::available_panes().await?;
let num_panes = panes.len() as u16;
save_panes_content(panes, &temp_panes_content_dir).await?;
save_panes_content(panes, &temp_panes_content_dir, num_lines_to_drop).await?;

(temp_panes_content_dir, num_panes)
};
Expand Down Expand Up @@ -87,13 +90,19 @@ pub async fn save<P: AsRef<Path>>(backup_dirpath: P) -> Result<(PathBuf, v1::Ove
async fn save_panes_content<P: AsRef<Path>>(
panes: Vec<tmux::pane::Pane>,
destination_dir: P,
num_lines_to_drop: usize,
) -> Result<()> {
let mut handles = Vec::new();
let detected_shells = vec!["zsh", "bash", "fish"];

for pane in panes {
let dest_dir = destination_dir.as_ref().to_path_buf();
// TODO: improve this heuristic, maybe with config
let drop_n_last_lines = if pane.command == "zsh" { 1 } else { 0 };

let drop_n_last_lines = if detected_shells.contains(&&pane.command[..]) {
num_lines_to_drop
} else {
0
};

let handle = task::spawn(async move {
let output = pane.capture(drop_n_last_lines).await.unwrap();
Expand Down
14 changes: 7 additions & 7 deletions src/bin/tmux-backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ async fn init_catalog<P: AsRef<Path>>(
Err(e) => {
failure_message(
format!(
"🛑 Catalog error at `{}`: {}",
backup_dirpath.as_ref().to_string_lossy(),
e
"🛑 Catalog error at `{}`: {e}",
backup_dirpath.as_ref().to_string_lossy()
),
Output::Both,
);
Expand Down Expand Up @@ -54,7 +53,7 @@ async fn run(config: Config) {
success_message(message, Output::Stdout)
}
Err(e) => failure_message(
format!("🛑 Could not compact backups: {}", e),
format!("🛑 Could not compact backups: {e}"),
Output::Stdout,
),
},
Expand All @@ -69,10 +68,11 @@ async fn run(config: Config) {
strategy,
to_tmux,
compact,
num_lines_to_drop,
} => {
let catalog = init_catalog(&config.backup_dirpath, strategy).await;

match save(&catalog.dirpath).await {
match save(&catalog.dirpath, num_lines_to_drop as usize).await {
Ok((backup_filepath, archive_overview)) => {
if compact {
// In practice this should never fail: write to the catalog already ensures
Expand All @@ -92,7 +92,7 @@ async fn run(config: Config) {
success_message(message, to_tmux);
}
Err(e) => {
failure_message(format!("🛑 Could not save sessions: {}", e), to_tmux);
failure_message(format!("🛑 Could not save sessions: {e}"), to_tmux);
}
};
}
Expand Down Expand Up @@ -124,7 +124,7 @@ async fn run(config: Config) {
success_message(message, to_tmux)
}
Err(e) => {
failure_message(format!("🛑 Could not restore sessions: {}", e), to_tmux);
failure_message(format!("🛑 Could not restore sessions: {e}"), to_tmux);
}
}
}
Expand Down
20 changes: 19 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl fmt::Display for StrategyValues {
Self::MostRecent => "most-recent",
Self::Classic => "classic",
};
write!(f, "{}", s)
write!(f, "{s}")
}
}

Expand Down Expand Up @@ -109,6 +109,24 @@ pub enum Command {
/// Delete purgeable backups after saving.
#[clap(long, action = ArgAction::SetTrue)]
compact: bool,

/// Number of lines to ignore during capture if the active command is a shell.
///
/// At the time of saving, for each pane where the active command is one of (`zsh`, `bash`,
/// `fish`), the shell prompt is waiting for input. If tmux-backup naively captures the
/// entire history, on restoring that backup, a new shell prompt will also appear. This
/// obviously pollutes history with repeated shell prompts.
///
/// If you know the number of lines your shell prompt occupies on screen, set this option
/// to that number (simply `1` in my case). These last lines will not be captured. On
/// restore, this gives the illusion of history continuity without repetition.
#[clap(
short = 'i',
long = "ignore-last-lines",
value_name = "NUMBER",
default_value_t = 0
)]
num_lines_to_drop: u8,
},

/// Restore the Tmux sessions from a backup file.
Expand Down
2 changes: 1 addition & 1 deletion src/management/archive/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Metadata {
/// Query Tmux and return a new `Metadata`.
pub async fn new() -> Result<Self> {
let version = FORMAT_VERSION.to_string();
let client = tmux::client::current_client().await?;
let client = tmux::client::current().await?;
let sessions = tmux::session::available_sessions().await?;
let windows = tmux::window::available_windows().await?;
let panes = tmux::pane::available_panes().await?;
Expand Down
2 changes: 1 addition & 1 deletion src/management/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl Backup {
return "1 minute".into();
}

format!("{} seconds", duration_secs)
format!("{duration_secs} seconds")
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/management/compaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl fmt::Display for Strategy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Strategy::KeepMostRecent { k } => {
write!(f, "KeepMostRecent: {}", k)
write!(f, "KeepMostRecent: {k}")
}
Strategy::Classic => write!(f, "Classic"),
}
Expand Down
8 changes: 4 additions & 4 deletions tmux-backup.tmux
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
#
# set -g @backup-keytable "foobar"
# set -g @backup-keyswitch "z"
# set -g @backup-strategy "-k 10"
# set -g @backup-strategy "-s most-recent -n 10"
#
# and bindings like
#
# bind-key -T foobar l 'tmux-backup -k 10 catalog list'
# bind-key -T foobar l 'tmux-backup catalog list'
#
# You can also entirely ignore this file (not even source it) and define all
# options and bindings in your `tmux.conf`.
Expand Down Expand Up @@ -76,9 +76,9 @@ function setup_binding_w_popup() {
}

# prefix + b + b only saves a new backup without compacting the catalog
setup_binding "b" "save ${strategy} --to-tmux"
setup_binding "b" "save ${strategy} --ignore-last-lines 1 --to-tmux"
# prefix + b + s saves a new backup and compacts the catalog
setup_binding "s" "save ${strategy} --compact --to-tmux"
setup_binding "s" "save ${strategy} --ignore-last-lines 1 --compact --to-tmux"
# prefix + b + r restores the most recent backup
setup_binding "r" "restore ${strategy} --to-tmux"
# prefix + b + l prints the catalog without details
Expand Down
6 changes: 5 additions & 1 deletion tmux-lib/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ impl FromStr for Client {
// ------------------------------

/// Return the current client useful attributes.
pub async fn current_client() -> Result<Client> {
///
/// # Errors
///
/// Returns an `io::IOError` in the command failed.
pub async fn current() -> Result<Client> {
let args = vec![
"display-message",
"-p",
Expand Down
13 changes: 12 additions & 1 deletion tmux-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ pub enum Error {
}

/// Convert a nom error into an owned error and add the parsing intent.
///
/// # Errors
///
/// This maps to a `Error::ParseError`.
#[must_use]
pub fn map_add_intent(
desc: &'static str,
intent: &'static str,
Expand All @@ -56,8 +61,14 @@ pub fn map_add_intent(
}
}

/// Ensure that the output's stdout and stderr are empty, indicating
/// the command had succeeded.
///
/// # Errors
///
/// Returns a `Error::UnexpectedTmuxOutput` in case .
pub fn check_empty_process_output(
output: Output,
output: &Output,
intent: &'static str,
) -> std::result::Result<(), Error> {
if !output.stdout.is_empty() || !output.stderr.is_empty() {
Expand Down
1 change: 1 addition & 0 deletions tmux-lib/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct WindowLayout {

impl WindowLayout {
/// Return a flat list of pane ids.
#[must_use]
pub fn pane_ids(&self) -> Vec<u16> {
let mut acc: Vec<u16> = vec![];
acc.reserve(1);
Expand Down
77 changes: 61 additions & 16 deletions tmux-lib/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,27 +105,45 @@ impl Pane {

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

let mut trimmed_lines: Vec<&[u8]> = output
.stdout
.split(|c| *c == b'\n')
.map(|line| line.trim_trailing())
.collect();

trimmed_lines.truncate(trimmed_lines.len() - drop_n_last_lines);
let trimmed_lines: Vec<&[u8]> = Self::buf_trim_trailing(&output.stdout);
let mut buffer: Vec<&[u8]> = Self::drop_last_empty_lines(&trimmed_lines);
buffer.truncate(buffer.len() - drop_n_last_lines);

// Join the lines with `b'\n'`, add reset code to the last line
let mut output_trimmed: Vec<u8> = Vec::with_capacity(output.stdout.len());
for (idx, &line) in trimmed_lines.iter().enumerate() {
output_trimmed.extend_from_slice(line);
if idx != trimmed_lines.len() - 1 {
output_trimmed.push(b'\n');
} else {
let mut final_buffer: Vec<u8> = Vec::with_capacity(output.stdout.len());
for (idx, &line) in buffer.iter().enumerate() {
final_buffer.extend_from_slice(line);

let is_last_line = idx == buffer.len() - 1;
if is_last_line {
let reset = "\u{001b}[0m".as_bytes();
output_trimmed.extend_from_slice(reset);
final_buffer.extend_from_slice(reset);
final_buffer.push(b'\n');
} else {
final_buffer.push(b'\n');
}
}

Ok(output_trimmed)
Ok(final_buffer)
}

/// Trim each line of the buffer.
pub(crate) fn buf_trim_trailing(buf: &[u8]) -> Vec<&[u8]> {
let trimmed_lines: Vec<&[u8]> = buf
.split(|c| *c == b'\n')
.map(SliceExt::trim_trailing) // trim each line
.collect();

trimmed_lines
}

/// Drop all the last empty lines.
pub(crate) fn drop_last_empty_lines<'a>(lines: &[&'a [u8]]) -> Vec<&'a [u8]> {
if let Some(last) = lines.iter().rposition(|line| !line.is_empty()) {
lines[0..=last].to_vec()
} else {
lines.to_vec()
}
}
}

Expand Down Expand Up @@ -228,7 +246,7 @@ pub async fn select_pane(pane_id: &PaneId) -> Result<()> {
let args = vec!["select-pane", "-t", pane_id.as_str()];

let output = Command::new("tmux").args(&args).output().await?;
check_empty_process_output(output, "select-pane")
check_empty_process_output(&output, "select-pane")
}

#[cfg(test)]
Expand Down Expand Up @@ -278,4 +296,31 @@ mod tests {

assert_eq!(panes, expected);
}

#[test]
fn test_buf_trim_trailing() {
let text = "line1\n\nline3 ";
let actual = Pane::buf_trim_trailing(text.as_bytes());
let expected = vec!["line1".as_bytes(), "".as_bytes(), "line3".as_bytes()];
assert_eq!(actual, expected);
}

#[test]
fn test_buf_drop_last_empty_lines() {
let text = "line1\nline2\n\nline3 ";

let trimmed_lines = Pane::buf_trim_trailing(text.as_bytes());
let actual = Pane::drop_last_empty_lines(&trimmed_lines);
let expected = trimmed_lines;
assert_eq!(actual, expected);

//

let text = "line1\nline2\n\n\n ";

let trimmed_lines = Pane::buf_trim_trailing(text.as_bytes());
let actual = Pane::drop_last_empty_lines(&trimmed_lines);
let expected = vec!["line1".as_bytes(), "line2".as_bytes()];
assert_eq!(actual, expected);
}
}
Loading

0 comments on commit 523c4dc

Please sign in to comment.