Skip to content

Commit

Permalink
feat(tmux): capture panes, windows & panes
Browse files Browse the repository at this point in the history
  • Loading branch information
graelo committed Aug 18, 2022
1 parent aa4b2c4 commit 89ccdb6
Show file tree
Hide file tree
Showing 6 changed files with 489 additions and 23 deletions.
13 changes: 9 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
use std::error::Error;
use std::fmt;

#[derive(Debug)]
pub enum ParseError {
ExpectedPaneIdMarker,
ExpectedIdMarker(char),
ExpectedInt(std::num::ParseIntError),
ExpectedBool(std::str::ParseBoolError),
ExpectedString(String),
ProcessFailure(String),
}

impl Error for ParseError {
fn description(&self) -> &str {
"ParseError"
}
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::ExpectedPaneIdMarker => write!(f, "Expected pane id marker"),
ParseError::ExpectedIdMarker(ch) => write!(f, "Expected id marker `{}`", ch),
ParseError::ExpectedInt(msg) => write!(f, "Expected an int: {}", msg),
ParseError::ExpectedBool(msg) => write!(f, "Expected a bool: {}", msg),
ParseError::ExpectedString(msg) => write!(f, "Expected {}", msg),
ParseError::ProcessFailure(msg) => write!(f, "{}", msg),
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
mod error;
mod tmux;

fn main() {
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
println!("Hello, world!");

println!("---- sessions ----");
let sessions = tmux::session::available_sessions()?;
for session in sessions {
println!("{:?}", session);
}

println!("---- windows ----");
let windows = tmux::available_windows()?;
for window in windows {
println!("{:?}", window);
}

println!("---- panes ----");
let panes = tmux::pane::available_panes()?;
for pane in panes {
println!("{:?}", pane);
}

Ok(())
}
37 changes: 36 additions & 1 deletion src/tmux/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
mod pane;
pub mod pane;
pub mod session;
pub mod window;

use std::str::FromStr;

use crate::error;
use session::Session;
use window::Window;

/// Returns a list of all `Window` from all sessions.
pub fn available_windows() -> Result<Vec<Window>, error::ParseError> {
let args = vec![
"list-windows",
"-a",
"-F",
"#{window_id}\
:#{window_index}\
:#{?window_active,true,false}\
:#{window_layout}\
:#{window_name}\
:#{window_linked_sessions_list}",
];

let output = duct::cmd("tmux", &args).read()?;

// Each call to `Window::parse` returns a `Result<Window, _>`. All results
// are collected into a Result<Vec<Window>, _>, thanks to `collect()`.
let result: Result<Vec<Window>, error::ParseError> = output
.trim_end() // trim last '\n' as it would create an empty line
.split('\n')
.map(|line| Window::from_str(line))
.collect();

result
}
34 changes: 17 additions & 17 deletions src/tmux/pane.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! This module provides types and functions to use Tmux.
//! This module provides a few types and functions to handle Tmux Panes.
//!
//! The main use cases are running Tmux commands & parsing Tmux panes
//! information.
Expand Down Expand Up @@ -56,7 +56,7 @@ impl FromStr for Pane {
/// This status line is obtained with
///
/// ```text
/// tmux list-panes -F "#{pane_id}:#{pane_index}:#{?pane_active,true,false}:#{pane_width}:#{pane_height}:#{pane_left}:#{pane_right}:#{pane_top}:#{pane_bottom}:#{pane_title}:#{pane_current_path}:#{pane_current_command}"`.
/// tmux list-panes -F "#{pane_id}:#{pane_index}:#{?pane_active,true,false}:#{pane_width}:#{pane_height}:#{pane_left}:#{pane_right}:#{pane_top}:#{pane_bottom}:#{pane_title}:#{pane_current_path}:#{pane_current_command}"
/// ```
///
/// For definitions, look at `Pane` type and the tmux man page for
Expand Down Expand Up @@ -129,7 +129,7 @@ impl Pane {
/// scrolled up by 3 lines. It is necessarily in copy mode. Its start line
/// index is `-3`. The index of the last line is `(40-1) - 3 = 36`.
///
pub fn capture_pane(&self) -> Result<String, ParseError> {
pub fn capture(&self) -> Result<String, ParseError> {
let args = vec![
"capture-pane",
"-t",
Expand All @@ -153,13 +153,12 @@ pub struct PaneId(String);
impl FromStr for PaneId {
type Err = ParseError;

/// Parse into PaneId. The `&str` must be start with '%'
/// followed by a `u32`.
/// 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(ParseError::ExpectedPaneIdMarker);
return Err(ParseError::ExpectedIdMarker('$'));
}
let id = src[1..].parse::<u32>()?;
let id = src[1..].parse::<u16>()?;
let id = format!("%{}", id);
Ok(PaneId(id))
}
Expand All @@ -177,19 +176,20 @@ impl fmt::Display for PaneId {
}
}

/// Returns a list of `Pane` from the current tmux session.
pub fn list_panes() -> Result<Vec<Pane>, ParseError> {
/// Returns a list of all `Pane` from all sessions.
pub fn available_panes() -> Result<Vec<Pane>, ParseError> {
let args = vec![
"list-panes",
"-a",
"-F",
"#{pane_id}:\
#{pane_index}\
:#{?pane_active,true,false}\
:#{pane_width}:#{pane_height}\
:#{pane_left}:#{pane_right}:#{pane_top}:#{pane_bottom}\
:#{pane_title}\
:#{pane_current_path}\
:#{pane_current_command}",
"#{pane_id}\
:#{pane_index}\
:#{?pane_active,true,false}\
:#{pane_width}:#{pane_height}\
:#{pane_left}:#{pane_right}:#{pane_top}:#{pane_bottom}\
:#{pane_title}\
:#{pane_current_path}\
:#{pane_current_command}",
];

let output = duct::cmd("tmux", &args).read()?;
Expand Down
142 changes: 142 additions & 0 deletions src/tmux/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! This module provides a few types and functions to handle Tmux sessions.
//!
//! The main use cases are running Tmux commands & parsing Tmux session
//! information.
use std::fmt;
use std::str::FromStr;

use crate::error::ParseError;

#[derive(Debug, PartialEq)]
pub struct Session {
/// Session identifier, e.g. `$3`.
pub id: SessionId,
/// Name of the session.
pub name: String,
}

#[derive(Debug, PartialEq)]
pub struct SessionId(String);

impl FromStr for SessionId {
type Err = ParseError;

/// Parse into SessionId. The `&str` must start with '$' followed by a
/// `u16`.
fn from_str(src: &str) -> Result<Self, Self::Err> {
if !src.starts_with('$') {
return Err(ParseError::ExpectedIdMarker('$'));
}
let id = src[1..].parse::<u16>()?;
let id = format!("${}", id);
Ok(SessionId(id))
}
}

impl SessionId {
pub fn as_str(&self) -> &str {
&self.0
}
}

impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}

impl FromStr for Session {
type Err = ParseError;

/// Parse a string containing tmux session status into a new `Session`.
///
/// This returns a `Result<Session, ParseError>` as this call can obviously
/// fail if provided an invalid format.
///
/// The expected format of the tmux status is
///
/// ```text
/// $1:pytorch
/// $2:rust
/// $3:swift
/// $4:tmux-hacking
/// ```
///
/// This status line is obtained with
///
/// ```text
/// tmux list-sessions -F "#{session_id}:#{session_name}"
/// ```
///
/// For definitions, look at `Session` type and the tmux man page for
/// definitions.
fn from_str(src: &str) -> Result<Self, Self::Err> {
let items: Vec<&str> = src.split(':').collect();
assert_eq!(items.len(), 2, "tmux should have returned 2 items per line");

let mut iter = items.iter();

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

let name = iter.next().unwrap().to_string();

Ok(Session { id, name })
}
}

/// Returns a list of all `Session` from the current tmux session.
pub fn available_sessions() -> Result<Vec<Session>, ParseError> {
let args = vec!["list-sessions", "-F", "#{session_id}:#{session_name}"];

let output = duct::cmd("tmux", &args).read()?;

// Each call to `Session::parse` returns a `Result<Session, _>`. All results
// are collected into a Result<Vec<Session>, _>, thanks to `collect()`.
let result: Result<Vec<Session>, ParseError> = output
.trim_end() // trim last '\n' as it would create an empty line
.split('\n')
.map(|line| Session::from_str(line))
.collect();

result
}

#[cfg(test)]
mod tests {
use super::Session;
use super::SessionId;
use crate::error;
use std::str::FromStr;

#[test]
fn parse_list_sessions() {
let output = vec!["$1:pytorch", "$2:rust", "$3:swift", "$4:tmux-hacking"];
let sessions: Result<Vec<Session>, error::ParseError> =
output.iter().map(|&line| Session::from_str(line)).collect();
let sessions = sessions.expect("Could not parse tmux sessions");

let expected = vec![
Session {
id: SessionId::from_str("$1").unwrap(),
name: String::from("pytorch"),
},
Session {
id: SessionId::from_str("$2").unwrap(),
name: String::from("rust"),
},
Session {
id: SessionId::from_str("$3").unwrap(),
name: String::from("swift"),
},
Session {
id: SessionId::from_str("$4").unwrap(),
name: String::from("tmux-hacking"),
},
];

assert_eq!(sessions, expected);
}
}
Loading

0 comments on commit 89ccdb6

Please sign in to comment.