diff --git a/src/error.rs b/src/error.rs index c26646e..e13a184 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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), } } diff --git a/src/main.rs b/src/main.rs index bac77e1..20514ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,28 @@ mod error; mod tmux; -fn main() { +use std::error::Error; + +fn main() -> Result<(), Box> { 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(()) } diff --git a/src/tmux/mod.rs b/src/tmux/mod.rs index e66d144..297cc06 100644 --- a/src/tmux/mod.rs +++ b/src/tmux/mod.rs @@ -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, 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`. All results + // are collected into a Result, _>, thanks to `collect()`. + let result: Result, 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 +} diff --git a/src/tmux/pane.rs b/src/tmux/pane.rs index 541d7cb..206fb7b 100644 --- a/src/tmux/pane.rs +++ b/src/tmux/pane.rs @@ -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. @@ -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 @@ -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 { + pub fn capture(&self) -> Result { let args = vec![ "capture-pane", "-t", @@ -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 { if !src.starts_with('%') { - return Err(ParseError::ExpectedPaneIdMarker); + return Err(ParseError::ExpectedIdMarker('$')); } - let id = src[1..].parse::()?; + let id = src[1..].parse::()?; let id = format!("%{}", id); Ok(PaneId(id)) } @@ -177,19 +176,20 @@ impl fmt::Display for PaneId { } } -/// Returns a list of `Pane` from the current tmux session. -pub fn list_panes() -> Result, ParseError> { +/// Returns a list of all `Pane` from all sessions. +pub fn available_panes() -> Result, 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()?; diff --git a/src/tmux/session.rs b/src/tmux/session.rs new file mode 100644 index 0000000..d3d7feb --- /dev/null +++ b/src/tmux/session.rs @@ -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 { + if !src.starts_with('$') { + return Err(ParseError::ExpectedIdMarker('$')); + } + let id = src[1..].parse::()?; + 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` 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 { + 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, 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`. All results + // are collected into a Result, _>, thanks to `collect()`. + let result: Result, 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, 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); + } +} diff --git a/src/tmux/window.rs b/src/tmux/window.rs new file mode 100644 index 0000000..35949f6 --- /dev/null +++ b/src/tmux/window.rs @@ -0,0 +1,262 @@ +//! This module provides a few types and functions to handle Tmux windows. +//! +//! The main use cases are running Tmux commands & parsing Tmux window +//! information. + +use std::fmt; +use std::str::FromStr; + +use crate::error::ParseError; + +#[derive(Debug, PartialEq)] +pub struct Window { + /// Window identifier, e.g. `@3`. + pub id: WindowId, + /// Index of the Window. + pub index: u16, + /// Describes whether the Window is active. + pub is_active: bool, + /// Describes how panes are laid out in the Window. + pub layout: String, + /// Name of the Window. + pub name: String, + /// Name of Sessions to which this Window is attached. + pub sessions: Vec, +} + +#[derive(Debug, PartialEq)] +pub struct WindowId(String); + +impl FromStr for WindowId { + type Err = ParseError; + + /// Parse into WindowId. The `&str` must start with '@' followed by a + /// `u16`. + fn from_str(src: &str) -> Result { + if !src.starts_with('@') { + return Err(ParseError::ExpectedIdMarker('@')); + } + let id = src[1..].parse::()?; + let id = format!("@{}", id); + Ok(WindowId(id)) + } +} + +impl WindowId { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for WindowId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for Window { + type Err = ParseError; + + /// Parse a string containing the tmux window status into a new `Window`. + /// + /// This returns a `Result` as this call can obviously + /// fail if provided an invalid format. + /// + /// The expected format of the tmux status is + /// + /// ```text + /// @1:0:true:035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}:ignite:pytorch + /// @2:1:false:4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]:dates-attn:pytorch + /// @3:2:false:9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}:th-bits:pytorch + /// @4:3:false:64ef,334x85,0,0,10:docker-pytorch:pytorch + /// @5:0:true:64f0,334x85,0,0,11:ben-williamson:rust + /// @6:1:false:64f1,334x85,0,0,12:pyo3:rust + /// @7:2:false:64f2,334x85,0,0,13:mdns-repeater:rust + /// @8:0:true:64f3,334x85,0,0,14:combine:swift + /// @9:0:false:64f4,334x85,0,0,15:copyrat:tmux-hacking + /// @10:1:false:ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]:mytui-app:tmux-hacking + /// @11:2:true:e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}:tmux-revive:tmux-hacking + /// ``` + /// + /// This status line is obtained with + /// + /// ```text + /// tmux list-windows -a -F "#{window_id}:#{window_index}:#{?window_active,true,false}:#{window_layout}:#{window_name}:#{window_linked_sessions_list}" + /// ``` + /// + /// For definitions, look at `Window` type and the tmux man page for + /// definitions. + fn from_str(src: &str) -> Result { + let items: Vec<&str> = src.split(':').collect(); + assert_eq!(items.len(), 6, "tmux should have returned 6 items per line"); + + let mut iter = items.iter(); + + // Window id must be start with '%' followed by a `u32` + let id_str = iter.next().unwrap(); + let id = WindowId::from_str(id_str)?; + + let index = iter.next().unwrap().parse::()?; + + let is_active = iter.next().unwrap().parse::()?; + + let layout = iter.next().unwrap().to_string(); + + let name = iter.next().unwrap().to_string(); + + let session_names = iter.next().unwrap().to_string(); + let sessions = vec![session_names]; + + Ok(Window { + id, + index, + is_active, + layout, + name, + sessions, + }) + } +} + +#[cfg(test)] +mod tests { + use super::Window; + use super::WindowId; + use crate::error; + use std::str::FromStr; + + #[test] + fn parse_list_sessions() { + let output = vec![ + "@1:0:true:035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}:ignite:pytorch", + "@2:1:false:4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]:dates-attn:pytorch", + "@3:2:false:9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}:th-bits:pytorch", + "@4:3:false:64ef,334x85,0,0,10:docker-pytorch:pytorch", + "@5:0:true:64f0,334x85,0,0,11:ben:rust", + "@6:1:false:64f1,334x85,0,0,12:pyo3:rust", + "@7:2:false:64f2,334x85,0,0,13:mdns-repeater:rust", + "@8:0:true:64f3,334x85,0,0,14:combine:swift", + "@9:0:false:64f4,334x85,0,0,15:copyrat:tmux-hacking", + "@10:1:false:ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]:mytui-app:tmux-hacking", + "@11:2:true:e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}:tmux-revive:tmux-hacking", + ]; + let sessions: Result, error::ParseError> = + output.iter().map(|&line| Window::from_str(line)).collect(); + let windows = sessions.expect("Could not parse tmux sessions"); + + let expected = vec![ + Window { + id: WindowId::from_str("@1").unwrap(), + index: 0, + is_active: true, + layout: String::from( + "035d,334x85,0,0{167x85,0,0,1,166x85,168,0[166x48,168,0,2,166x36,168,49,3]}", + ), + name: String::from("ignite"), + sessions: vec![String::from("pytorch")], + }, + Window { + id: WindowId::from_str("@2").unwrap(), + index: 1, + is_active: false, + layout: String::from( + "4438,334x85,0,0[334x41,0,0{167x41,0,0,4,166x41,168,0,5},334x43,0,42{167x43,0,42,6,166x43,168,42,7}]", + ), + name: String::from("dates-attn"), + sessions: vec![String::from("pytorch")], + }, + Window { + id: WindowId::from_str("@3").unwrap(), + index: 2, + is_active: false, + layout: String::from( + "9e8b,334x85,0,0{167x85,0,0,8,166x85,168,0,9}", + ), + name: String::from("th-bits"), + sessions: vec![String::from("pytorch")], + }, + Window { + id: WindowId::from_str("@4").unwrap(), + index: 3, + is_active: false, + layout: String::from( + "64ef,334x85,0,0,10", + ), + name: String::from("docker-pytorch"), + sessions: vec![String::from("pytorch")], + }, + Window { + id: WindowId::from_str("@5").unwrap(), + index: 0, + is_active: true, + layout: String::from( + "64f0,334x85,0,0,11", + ), + name: String::from("ben"), + sessions: vec![String::from("rust")], + }, + Window { + id: WindowId::from_str("@6").unwrap(), + index: 1, + is_active: false, + layout: String::from( + "64f1,334x85,0,0,12", + ), + name: String::from("pyo3"), + sessions: vec![String::from("rust")], + }, + Window { + id: WindowId::from_str("@7").unwrap(), + index: 2, + is_active: false, + layout: String::from( + "64f2,334x85,0,0,13", + ), + name: String::from("mdns-repeater"), + sessions: vec![String::from("rust")], + }, + Window { + id: WindowId::from_str("@8").unwrap(), + index: 0, + is_active: true, + layout: String::from( + "64f3,334x85,0,0,14", + ), + name: String::from("combine"), + sessions: vec![String::from("swift")], + }, + Window { + id: WindowId::from_str("@9").unwrap(), + index: 0, + is_active: false, + layout: String::from( + "64f4,334x85,0,0,15", + ), + name: String::from("copyrat"), + sessions: vec![String::from("tmux-hacking")], + }, + Window { + id: WindowId::from_str("@10").unwrap(), + index: 1, + is_active: false, + layout: String::from( + "ae3a,334x85,0,0[334x48,0,0,17,334x36,0,49{175x36,0,49,18,158x36,176,49,19}]", + ), + name: String::from("mytui-app"), + sessions: vec![String::from("tmux-hacking")], + }, + Window { + id: WindowId::from_str("@11").unwrap(), + index: 2, + is_active: true, + layout: String::from( + "e2e2,334x85,0,0{175x85,0,0,20,158x85,176,0[158x42,176,0,21,158x42,176,43,27]}", + ), + name: String::from("tmux-revive"), + sessions: vec![String::from("tmux-hacking")], + }, + ]; + + assert_eq!(windows, expected); + } +}