2020-05-26 08:11:45 +02:00
use clap ::Clap ;
2020-05-25 23:06:00 +02:00
use regex ::Regex ;
use std ::collections ::HashMap ;
2020-05-27 10:04:42 +02:00
use std ::str ::FromStr ;
2020-05-25 23:06:00 +02:00
2020-05-27 10:04:42 +02:00
use copyrat ::error ::ParseError ;
use copyrat ::process ;
2020-05-25 23:06:00 +02:00
#[ derive(Debug, PartialEq) ]
pub struct Pane {
/// Pane identifier.
pub id : u32 ,
/// Describes if the pane is in some mode.
pub in_mode : bool ,
/// Number of lines in the pane.
pub height : u32 ,
/// Optional offset from the bottom if the pane is in some mode.
///
/// When a pane is in copy mode, scrolling up changes the
/// `scroll_position`. If the pane is in normal mode, or unscrolled,
/// then `0` is returned.
pub scroll_position : u32 ,
/// Describes if the pane is currently active (focused).
pub is_active : bool ,
}
impl Pane {
/// Parse a string containing tmux panes status into a new `Pane`.
///
/// This returns a `Result<Pane, ParseError>` as this call can obviously
/// fail if provided an invalid format.
///
/// The expected format of the tmux status is "%52:false:62:3:false",
/// or "%53:false:23::true".
///
/// This status line is obtained with `tmux list-panes -F '#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}'`.
///
/// For definitions, look at `Pane` type,
/// and at the tmux man page for definitions.
pub fn parse ( src : & str ) -> Result < Pane , ParseError > {
let items : Vec < & str > = src . split ( ':' ) . collect ( ) ;
assert_eq! ( items . len ( ) , 5 , " tmux should have returned 5 items per line " ) ;
let mut iter = items . iter ( ) ;
let id_str = iter . next ( ) . unwrap ( ) ;
if ! id_str . starts_with ( '%' ) {
return Err ( ParseError ::ExpectedPaneIdMarker ) ;
}
let id = id_str [ 1 .. ] . parse ::< u32 > ( ) ? ;
let in_mode = iter . next ( ) . unwrap ( ) . parse ::< bool > ( ) ? ;
let height = iter . next ( ) . unwrap ( ) . parse ::< u32 > ( ) ? ;
let scroll_position = iter . next ( ) . unwrap ( ) ;
let scroll_position = if scroll_position . is_empty ( ) {
" 0 "
} else {
scroll_position
} ;
let scroll_position = scroll_position . parse ::< u32 > ( ) ? ;
let is_active = iter . next ( ) . unwrap ( ) . parse ::< bool > ( ) ? ;
Ok ( Pane {
id ,
in_mode ,
height ,
scroll_position ,
is_active ,
} )
}
}
/// Returns a list of `Pane` from the current tmux session.
pub fn list_panes ( ) -> Result < Vec < Pane > , ParseError > {
let args = vec! [
" list-panes " ,
" -F " ,
2020-05-28 07:07:51 +02:00
" #{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false} " ,
2020-05-25 23:06:00 +02:00
] ;
2020-05-27 10:04:42 +02:00
let output = process ::execute ( " tmux " , & args ) ? ;
2020-05-25 23:06:00 +02:00
// Each call to `Pane::parse` returns a `Result<Pane, _>`. All results
// are collected into a Result<Vec<Pane>, _>, thanks to `collect()`.
2020-05-28 07:07:51 +02:00
let result : Result < Vec < Pane > , ParseError > = output
. trim_end ( ) // trim last '\n' as it would create an empty line
. split ( '\n' )
. map ( | line | Pane ::parse ( line ) )
. collect ( ) ;
2020-05-25 23:06:00 +02:00
result
}
/// Returns tmux global options as a `HashMap`. The prefix argument is for
/// convenience, in order to target only some of our options. For instance,
/// `get_options("@copyrat-")` will return a `HashMap` which keys are tmux options names like `@copyrat-command`, and associated values.
///
/// # Example
/// ```get_options("@copyrat-")```
pub fn get_options ( prefix : & str ) -> Result < HashMap < String , String > , ParseError > {
let args = vec! [ " show " , " -g " ] ;
2020-05-27 10:04:42 +02:00
let output = process ::execute ( " tmux " , & args ) ? ;
2020-05-25 23:06:00 +02:00
let lines : Vec < & str > = output . split ( '\n' ) . collect ( ) ;
let pattern = format! ( r # "{prefix}([\w\-0-9]+) "?(\w+)"?"# , prefix = prefix ) ;
let re = Regex ::new ( & pattern ) . unwrap ( ) ;
let args : HashMap < String , String > = lines
. iter ( )
. flat_map ( | line | match re . captures ( line ) {
None = > None ,
Some ( captures ) = > {
let key = captures [ 1 ] . to_string ( ) ;
let value = captures [ 2 ] . to_string ( ) ;
Some ( ( key , value ) )
}
} )
. collect ( ) ;
Ok ( args )
}
2020-05-26 08:11:45 +02:00
#[ derive(Clap, Debug) ]
2020-05-25 23:06:00 +02:00
pub enum CaptureRegion {
/// The entire history.
///
/// This will end up sending `-S - -E -` to `tmux capture-pane`.
EntireHistory ,
2020-05-26 08:11:45 +02:00
/// The visible area.
VisibleArea ,
///// Region from start line to end line
/////
///// This works as defined in tmux's docs (order does not matter).
//Region(i32, i32),
2020-05-25 23:06:00 +02:00
}
2020-05-27 10:04:42 +02:00
impl FromStr for CaptureRegion {
type Err = ParseError ;
fn from_str ( s : & str ) -> Result < Self , ParseError > {
match s {
" leading " = > Ok ( CaptureRegion ::EntireHistory ) ,
" trailing " = > Ok ( CaptureRegion ::VisibleArea ) ,
_ = > Err ( ParseError ::ExpectedString ( String ::from (
" entire-history or visible-area " ,
) ) ) ,
}
}
}
2020-05-26 08:11:45 +02:00
/// Returns the entire Pane content as a `String`.
///
/// `CaptureRegion` specifies if the visible area is captured, or the entire
/// history.
2020-05-25 23:06:00 +02:00
///
2020-05-28 09:24:33 +02:00
/// # TODO
///
/// Capture with `capture-pane -J` joins wrapped lines.
///
2020-05-25 23:06:00 +02:00
/// # Note
///
/// If the pane is in normal mode, capturing the visible area can be done
/// without extra arguments (default behavior of `capture-pane`), but if the
/// pane is in copy mode, we need to take into account the current scroll
/// position. To support both cases, the implementation always provides those
/// parameters to tmux.
2020-05-26 08:11:45 +02:00
pub fn capture_pane ( pane : & Pane , region : & CaptureRegion ) -> Result < String , ParseError > {
2020-05-28 07:07:51 +02:00
let mut args = format! ( " capture-pane -t % {id} -p " , id = pane . id ) ;
2020-05-25 23:06:00 +02:00
let region_str = match region {
2020-05-26 08:11:45 +02:00
CaptureRegion ::VisibleArea = > {
2020-05-25 23:06:00 +02:00
// Will capture the visible area.
// Providing start/end helps support both copy and normal modes.
format! (
" -S {start} -E {end} " ,
start = pane . scroll_position ,
end = pane . height - pane . scroll_position - 1
)
}
2020-05-26 08:11:45 +02:00
CaptureRegion ::EntireHistory = > String ::from ( " -S - -E - " ) ,
2020-05-25 23:06:00 +02:00
} ;
args . push_str ( & region_str ) ;
let args : Vec < & str > = args . split ( ' ' ) . collect ( ) ;
2020-05-27 10:04:42 +02:00
let output = process ::execute ( " tmux " , & args ) ? ;
2020-05-25 23:06:00 +02:00
Ok ( output )
// format!(
// "tmux capture-pane -t {} -p{} | {}/target/release/thumbs -f '%U:%H' -t {} {}; tmux swap-pane -t {}; tmux wait-for -S {}",
// active_pane_id,
// scroll_params,
}
2020-05-28 09:24:33 +02:00
/// Creates a new named window in the background (without switching to it)
/// executing the provided command (probably `sh`) and returns a `Pane`
/// describing the newly created pane.
2020-05-28 07:07:51 +02:00
///
/// # Note
///
/// Returning a new `Pane` seems overkill, given we mostly take care of its
/// Id, but it is cleaner.
2020-05-28 09:24:33 +02:00
pub fn create_new_window ( name : & str , command : & str ) -> Result < Pane , ParseError > {
2020-05-28 07:07:51 +02:00
let args = vec! [ " new-window " , " -P " , " -d " , " -n " , name , " -F " ,
2020-05-28 09:24:33 +02:00
" #{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false} " ,
command ] ;
2020-05-28 07:07:51 +02:00
let output = process ::execute ( " tmux " , & args ) ? ;
let pane = Pane ::parse ( output . trim_end ( ) ) ? ; // trim last '\n' as it would create an empty line
Ok ( pane )
}
/// Ask tmux to swap two `Pane`s and change the active pane to be the target
/// `Pane`.
pub fn swap_panes ( pane_a : & Pane , pane_b : & Pane ) -> Result < ( ) , ParseError > {
let pa_id = format! ( " % {} " , pane_a . id ) ;
let pb_id = format! ( " % {} " , pane_b . id ) ;
let args = vec! [ " swap-pane " , " -s " , & pa_id , " -t " , & pb_id ] ;
process ::execute ( " tmux " , & args ) ? ;
Ok ( ( ) )
}
/// Ask tmux to kill the provided `Pane`.
pub fn kill_pane ( pane : & Pane ) -> Result < ( ) , ParseError > {
let p_id = format! ( " % {} " , pane . id ) ;
let args = vec! [ " kill-pane " , " -t " , & p_id ] ;
process ::execute ( " tmux " , & args ) ? ;
Ok ( ( ) )
}
2020-05-25 23:06:00 +02:00
#[ cfg(test) ]
mod tests {
use super ::Pane ;
use copyrat ::error ;
#[ test ]
fn test_parse_pass ( ) {
let output = vec! [ " %52:false:62:3:false " , " %53:false:23::true " ] ;
let panes : Result < Vec < Pane > , error ::ParseError > =
output . iter ( ) . map ( | & line | Pane ::parse ( line ) ) . collect ( ) ;
let panes = panes . expect ( " Could not parse tmux panes " ) ;
let expected = vec! [
Pane {
id : 52 ,
in_mode : false ,
height : 62 ,
scroll_position : 3 ,
is_active : false ,
} ,
Pane {
id : 53 ,
in_mode : false ,
height : 23 ,
scroll_position : 0 ,
is_active : true ,
} ,
] ;
assert_eq! ( panes , expected ) ;
}
}