use std::path::PathBuf;
use std::str::FromStr;
use nom::{
IResult, Parser,
character::complete::{char, digit1, not_line_ending},
combinator::{all_consuming, map_res},
};
use serde::{Deserialize, Serialize};
use smol::process::Command;
use crate::{
Result,
error::{Error, check_empty_process_output, check_process_success, map_add_intent},
pane_id::{PaneId, parse::pane_id},
parse::{boolean, quoted_nonempty_string, quoted_string},
window_id::WindowId,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pane {
pub id: PaneId,
pub index: u16,
pub is_active: bool,
pub title: String,
pub dirpath: PathBuf,
pub command: String,
}
impl FromStr for Pane {
type Err = Error;
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
let desc = "Pane";
let intent = "##{pane_id}:##{pane_index}:##{?pane_active,true,false}:'##{pane_title}':'##{pane_current_command}':##{pane_current_path}";
let (_, pane) = all_consuming(parse::pane)
.parse(input)
.map_err(|e| map_add_intent(desc, intent, e))?;
Ok(pane)
}
}
impl Pane {
pub async fn capture(&self) -> Result<Vec<u8>> {
let args = vec![
"capture-pane",
"-t",
self.id.as_str(),
"-J", "-e", "-p", "-S", "-", "-E", "-", ];
let output = Command::new("tmux").args(&args).output().await?;
Ok(output.stdout)
}
}
pub(crate) mod parse {
use super::*;
pub(crate) fn pane(input: &str) -> IResult<&str, Pane> {
let (input, (id, _, index, _, is_active, _, title, _, command, _, dirpath)) = (
pane_id,
char(':'),
map_res(digit1, str::parse),
char(':'),
boolean,
char(':'),
quoted_string,
char(':'),
quoted_nonempty_string,
char(':'),
not_line_ending,
)
.parse(input)?;
Ok((
input,
Pane {
id,
index,
is_active,
title: title.into(),
dirpath: dirpath.into(),
command: command.into(),
},
))
}
}
pub async fn available_panes() -> Result<Vec<Pane>> {
let args = vec![
"list-panes",
"-a",
"-F",
"#{pane_id}\
:#{pane_index}\
:#{?pane_active,true,false}\
:'#{pane_title}'\
:'#{pane_current_command}'\
:#{pane_current_path}",
];
let output = Command::new("tmux").args(&args).output().await?;
let buffer = String::from_utf8(output.stdout)?;
let result: Result<Vec<Pane>> = buffer
.trim_end() .split('\n')
.map(Pane::from_str)
.collect();
result
}
pub async fn new_pane(
reference_pane: &Pane,
pane_command: Option<&str>,
window_id: &WindowId,
) -> Result<PaneId> {
let mut args = vec![
"split-window",
"-h",
"-c",
reference_pane.dirpath.to_str().unwrap(),
"-t",
window_id.as_str(),
"-P",
"-F",
"#{pane_id}",
];
if let Some(pane_command) = pane_command {
args.push(pane_command);
}
let output = Command::new("tmux").args(&args).output().await?;
check_process_success(&output, "split-window")?;
let buffer = String::from_utf8(output.stdout)?;
let new_id = PaneId::from_str(buffer.trim_end())?;
Ok(new_id)
}
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")
}
#[cfg(test)]
mod tests {
use super::Pane;
use super::PaneId;
use crate::Result;
use std::path::PathBuf;
use std::str::FromStr;
#[test]
fn parse_list_panes() {
let output = [
"%20:0:false:'rmbp':'nvim':/Users/graelo/code/rust/tmux-backup",
"%21:1:true:'graelo@server: ~':'tmux':/Users/graelo/code/rust/tmux-backup",
"%27:2:false:'rmbp':'man man':/Users/graelo/code/rust/tmux-backup",
];
let panes: Result<Vec<Pane>> = output.iter().map(|&line| Pane::from_str(line)).collect();
let panes = panes.expect("Could not parse tmux panes");
let expected = vec![
Pane {
id: PaneId::from_str("%20").unwrap(),
index: 0,
is_active: false,
title: String::from("rmbp"),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("nvim"),
},
Pane {
id: PaneId(String::from("%21")),
index: 1,
is_active: true,
title: String::from("graelo@server: ~"),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("tmux"),
},
Pane {
id: PaneId(String::from("%27")),
index: 2,
is_active: false,
title: String::from("rmbp"),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("man man"),
},
];
assert_eq!(panes, expected);
}
#[test]
fn parse_pane_with_empty_title() {
let line = "%20:0:false:'':'nvim':/Users/graelo/code/rust/tmux-backup";
let pane = Pane::from_str(line).expect("Could not parse pane with empty title");
let expected = Pane {
id: PaneId::from_str("%20").unwrap(),
index: 0,
is_active: false,
title: String::from(""),
dirpath: PathBuf::from_str("/Users/graelo/code/rust/tmux-backup").unwrap(),
command: String::from("nvim"),
};
assert_eq!(pane, expected);
}
#[test]
fn parse_pane_with_large_index() {
let line = "%999:99:true:'host':'zsh':/home/user";
let pane = Pane::from_str(line).expect("Should parse pane with large index");
assert_eq!(pane.id, PaneId::from_str("%999").unwrap());
assert_eq!(pane.index, 99);
assert!(pane.is_active);
}
#[test]
fn parse_pane_with_spaces_in_path() {
let line = "%1:0:false:'title':'vim':/Users/user/My Documents/project";
let pane = Pane::from_str(line).expect("Should parse pane with spaces in path");
assert_eq!(
pane.dirpath,
PathBuf::from("/Users/user/My Documents/project")
);
}
#[test]
fn parse_pane_with_unicode_title() {
let line = "%1:0:true:'日本語タイトル':'bash':/home/user";
let pane = Pane::from_str(line).expect("Should parse pane with unicode title");
assert_eq!(pane.title, "日本語タイトル");
}
#[test]
fn parse_pane_with_complex_command() {
let line = "%1:0:false:'host':'python -m http.server 8080':/tmp";
let pane = Pane::from_str(line).expect("Should parse pane with complex command");
assert_eq!(pane.command, "python -m http.server 8080");
}
#[test]
fn parse_pane_fails_on_missing_id() {
let line = "0:false:'title':'cmd':/path";
let result = Pane::from_str(line);
assert!(result.is_err());
}
#[test]
fn parse_pane_fails_on_invalid_boolean() {
let line = "%1:0:yes:'title':'cmd':/path";
let result = Pane::from_str(line);
assert!(result.is_err());
}
#[test]
fn parse_pane_fails_on_empty_command() {
let line = "%1:0:true:'title':'':/path";
let result = Pane::from_str(line);
assert!(result.is_err());
}
#[test]
fn parse_pane_fails_on_missing_path() {
let line = "%1:0:true:'title':'cmd'";
let result = Pane::from_str(line);
assert!(result.is_err());
}
#[test]
fn parse_pane_fails_on_wrong_id_prefix() {
let line = "@1:0:true:'title':'cmd':/path";
let result = Pane::from_str(line);
assert!(result.is_err());
}
}