diff --git a/Cargo.lock b/Cargo.lock index 8ccbd6a..e81c5ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -979,6 +979,8 @@ dependencies = [ [[package]] name = "tmux-lib" version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746097ec40eee5d978503883fd8d38208f4e1e065619b03f7f9330418644f144" dependencies = [ "async-std", "nom", diff --git a/Cargo.toml b/Cargo.toml index b07871c..03b1ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,3 @@ -[workspace] -members = [ - "tmux-lib" -] - [package] name = "tmux-backup" version = "0.4.0" @@ -38,7 +33,7 @@ si-scale = "0.2" futures = "0.3" async-std = { version = "1", features = ["unstable"] } -tmux-lib = { version = "0.2.1", path = "./tmux-lib" } +tmux-lib = { version = "0.2.1" } # archive ser/deser tempfile = "3" diff --git a/tmux-lib/Cargo.toml b/tmux-lib/Cargo.toml deleted file mode 100644 index 880696b..0000000 --- a/tmux-lib/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "tmux-lib" -version = "0.2.1" -edition = "2021" -rust-version = "1.59.0" -description = "Tmux helper functions." -readme = "README.md" - -license = "MIT OR Apache-2.0" -authors = ["graelo "] -repository = "https://github.com/graelo/tmux-backup" -homepage = "https://github.com/graelo/tmux-backup/tmux-lib" -documentation = "https://docs.rs/tmux-lib" - -keywords = ["tmux", "tmux-plugin", "tmux-resurrect", "backup"] -categories = ["command-line-utilities"] -exclude = ["/.github", "/ci", "/.travis.yml", "/appveyor.yml"] - -[dependencies] -thiserror = "1" -async-std = { version = "1", features = ["unstable"] } -serde = { version = "1.0", features = ["derive"] } -nom = "7" diff --git a/tmux-lib/LICENSE-APACHE b/tmux-lib/LICENSE-APACHE deleted file mode 100644 index 271297c..0000000 --- a/tmux-lib/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022 graelo - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/tmux-lib/LICENSE-MIT b/tmux-lib/LICENSE-MIT deleted file mode 100644 index 732190d..0000000 --- a/tmux-lib/LICENSE-MIT +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2022 graelo - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/tmux-lib/README.md b/tmux-lib/README.md deleted file mode 100644 index e5ffc2f..0000000 --- a/tmux-lib/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# `tmux-lib` - -[![crate](https://img.shields.io/crates/v/tmux-lib.svg)](https://crates.io/crates/tmux-lib) -[![documentation](https://docs.rs/tmux-lib/badge.svg)](https://docs.rs/tmux-lib) -[![minimum rustc 1.8](https://img.shields.io/badge/rustc-1.50+-red.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) -[![build status](https://github.com/graelo/tmux-backup/workflows/main/badge.svg)](https://github.com/graelo/tmux-backup/actions) - - - -Read or manipulate Tmux. - -Version requirement: _rustc 1.59.0+_ - -```toml -[dependencies] -tmux-lib = "0.2" -``` - -## Getting started - -Work in progress - -## Caveats - -- This is a beta version - -## License - -Licensed under either of - -- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) -- [MIT license](http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall -be dual licensed as above, without any additional terms or conditions. - - diff --git a/tmux-lib/src/client.rs b/tmux-lib/src/client.rs deleted file mode 100644 index afd8628..0000000 --- a/tmux-lib/src/client.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Client-level functions: for representing client state (`client_session` etc) or reporting information inside Tmux. - -use std::str::FromStr; - -use async_std::process::Command; -use nom::{character::complete::char, combinator::all_consuming, sequence::tuple}; -use serde::{Deserialize, Serialize}; - -use crate::{ - error::{map_add_intent, Error}, - parse::{quoted_nonempty_string, quoted_string}, - Result, -}; - -/// A Tmux client. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Client { - /// The current session. - pub session_name: String, - /// The last session. - pub last_session_name: String, -} - -impl FromStr for Client { - type Err = Error; - - /// Parse a string containing client information into a new `Client`. - /// - /// This returns a `Result` as this call can obviously - /// fail if provided an invalid format. - /// - /// The expected format of the tmux response is - /// - /// ```text - /// name-of-current-session:name-of-last-session - /// ``` - /// - /// This status line is obtained with - /// - /// ```text - /// tmux display-message -p -F "'#{client_session}':'#{client_last_session}'" - /// ``` - /// - /// For definitions, look at `Pane` type and the tmux man page for - /// definitions. - fn from_str(input: &str) -> std::result::Result { - let desc = "Client"; - let intent = "'#{client_session}':'#{client_last_session}'"; - let parser = tuple((quoted_nonempty_string, char(':'), quoted_string)); - - let (_, (session_name, _, last_session_name)) = - all_consuming(parser)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(Client { - session_name: session_name.to_string(), - last_session_name: last_session_name.to_string(), - }) - } -} - -// ------------------------------ -// Ops -// ------------------------------ - -/// Return the current client useful attributes. -/// -/// # Errors -/// -/// Returns an `io::IOError` in the command failed. -pub async fn current() -> Result { - let args = vec![ - "display-message", - "-p", - "-F", - "'#{client_session}':'#{client_last_session}'", - ]; - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - - Client::from_str(buffer.trim_end()) -} - -/// Return a list of all `Pane` from all sessions. -/// -/// # Panics -/// -/// This function panics if it can't communicate with Tmux. -pub fn display_message(message: &str) { - let args = vec!["display-message", message]; - - std::process::Command::new("tmux") - .args(&args) - .output() - .expect("Cannot communicate with Tmux for displaying message"); -} - -/// Switch to session exactly named `session_name`. - -pub async fn switch_client(session_name: &str) -> Result<()> { - let exact_session_name = format!("={session_name}"); - let args = vec!["switch-client", "-t", &exact_session_name]; - - Command::new("tmux") - .args(&args) - .output() - .await - .expect("Cannot communicate with Tmux for switching the client"); - - Ok(()) -} diff --git a/tmux-lib/src/error.rs b/tmux-lib/src/error.rs deleted file mode 100644 index c75fa59..0000000 --- a/tmux-lib/src/error.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{io, process::Output}; - -/// Describes all errors variants from this crate. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// A tmux invocation returned some output where none was expected (actions such as - /// some `tmux display-message` invocations). - #[error( - "unexpected process output: intent: `{intent}`, stdout: `{stdout}`, stderr: `{stderr}`" - )] - UnexpectedTmuxOutput { - intent: &'static str, - stdout: String, - stderr: String, - }, - - /// Indicates Tmux has a weird config, like missing the `"default-shell"`. - #[error("unexpected tmux config: `{0}`")] - TmuxConfig(&'static str), - - /// Some parsing error. - #[error("failed parsing: `{intent}`")] - ParseError { - desc: &'static str, - intent: &'static str, - err: nom::Err>, - }, - - /// Failed parsing the output of a process invocation as utf-8. - #[error("failed parsing utf-8 string: `{source}`")] - Utf8 { - #[from] - /// Source error. - source: std::string::FromUtf8Error, - }, - - /// Some IO error. - #[error("failed with io: `{source}`")] - Io { - #[from] - /// Source error. - source: io::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, - nom_err: nom::Err>, -) -> Error { - Error::ParseError { - desc, - intent, - err: nom_err.to_owned(), - } -} - -/// 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, - intent: &'static str, -) -> std::result::Result<(), Error> { - if !output.stdout.is_empty() || !output.stderr.is_empty() { - let stdout = String::from_utf8_lossy(&output.stdout[..]).to_string(); - let stderr = String::from_utf8_lossy(&output.stderr[..]).to_string(); - return Err(Error::UnexpectedTmuxOutput { - intent, - stdout, - stderr, - }); - } - Ok(()) -} diff --git a/tmux-lib/src/layout.rs b/tmux-lib/src/layout.rs deleted file mode 100644 index 5a0b5ec..0000000 --- a/tmux-lib/src/layout.rs +++ /dev/null @@ -1,354 +0,0 @@ -//! Parse the window layout string. -//! -//! Tmux reports the layout for a window, it can also use it to apply an existing layout to a -//! window. -//! -//! A window layout has this format: -//! -//! ```text -//! "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]" -//! ``` -//! -//! The parser in this module returns the corresponding [`WindowLayout`]. - -use nom::{ - branch::alt, - character::complete::{char, digit1, hex_digit1}, - combinator::{all_consuming, map_res}, - multi::separated_list1, - sequence::{delimited, tuple}, - IResult, -}; - -use crate::{error::map_add_intent, Result}; - -/// Represent a parsed window layout. -#[derive(Debug, PartialEq, Eq)] -pub struct WindowLayout { - /// 4-char hex id, such as `9f58`. - id: u16, - /// Container. - container: Container, -} - -impl WindowLayout { - /// Return a flat list of pane ids. - #[must_use] - pub fn pane_ids(&self) -> Vec { - let mut acc: Vec = vec![]; - acc.reserve(1); - self.walk(&mut acc); - acc - } - - /// Walk the structure, searching for pane ids. - fn walk(&self, acc: &mut Vec) { - self.container.walk(acc); - } -} - -#[derive(Debug, PartialEq, Eq)] -struct Container { - /// Dimensions of the container. - dimensions: Dimensions, - /// Offset of the top left corner of the container. - coordinates: Coordinates, - /// Either a pane, or a horizontal or vertical split. - element: Element, -} - -impl Container { - /// Walk the structure, searching for pane ids. - fn walk(&self, acc: &mut Vec) { - self.element.walk(acc); - } -} - -#[derive(Debug, PartialEq, Eq)] -struct Dimensions { - /// Width (of the window or pane). - width: u16, - /// Height (of the window or pane). - height: u16, -} - -#[derive(Debug, PartialEq, Eq)] -struct Coordinates { - /// Horizontal offset of the top left corner (of the window or pane). - x: u16, - /// Vertical offset of the top left corner (of the window or pane). - y: u16, -} - -/// Element in a container. -#[derive(Debug, PartialEq, Eq)] -enum Element { - /// A pane. - Pane { pane_id: u16 }, - /// A horizontal split. - Horizontal(Split), - /// A vertical split. - Vertical(Split), -} - -impl Element { - /// Walk the structure, searching for pane ids. - fn walk(&self, acc: &mut Vec) { - match self { - Self::Pane { pane_id } => acc.push(*pane_id), - Self::Horizontal(split) | Self::Vertical(split) => { - split.walk(acc); - } - } - } -} - -#[derive(Debug, PartialEq, Eq)] -struct Split { - /// Embedded containers. - elements: Vec, -} - -impl Split { - /// Walk the structure, searching for pane ids. - fn walk(&self, acc: &mut Vec) { - for element in &self.elements { - element.walk(acc); - } - } -} - -/// Parse the Tmux layout string description and return the pane-ids. -pub fn parse_window_layout(input: &str) -> Result { - let desc = "window-layout"; - let intent = "window-layout"; - let (_, win_layout) = - all_consuming(window_layout)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(win_layout) -} - -pub(crate) fn window_layout(input: &str) -> IResult<&str, WindowLayout> { - let (input, (id, _, container)) = tuple((layout_id, char(','), container))(input)?; - Ok((input, WindowLayout { id, container })) -} - -fn from_hex(input: &str) -> std::result::Result { - u16::from_str_radix(input, 16) -} - -fn layout_id(input: &str) -> IResult<&str, u16> { - map_res(hex_digit1, from_hex)(input) -} - -fn parse_u16(input: &str) -> IResult<&str, u16> { - map_res(digit1, str::parse)(input) -} - -fn dimensions(input: &str) -> IResult<&str, Dimensions> { - let (input, (width, _, height)) = tuple((parse_u16, char('x'), parse_u16))(input)?; - Ok((input, Dimensions { width, height })) -} - -fn coordinates(input: &str) -> IResult<&str, Coordinates> { - let (input, (x, _, y)) = tuple((parse_u16, char(','), parse_u16))(input)?; - Ok((input, Coordinates { x, y })) -} - -fn single_pane(input: &str) -> IResult<&str, Element> { - let (input, (_, pane_id)) = tuple((char(','), parse_u16))(input)?; - Ok((input, Element::Pane { pane_id })) -} - -fn horiz_split(input: &str) -> IResult<&str, Element> { - let (input, elements) = - delimited(char('{'), separated_list1(char(','), container), char('}'))(input)?; - Ok((input, Element::Horizontal(Split { elements }))) -} - -fn vert_split(input: &str) -> IResult<&str, Element> { - let (input, elements) = - delimited(char('['), separated_list1(char(','), container), char(']'))(input)?; - Ok((input, Element::Vertical(Split { elements }))) -} - -fn element(input: &str) -> IResult<&str, Element> { - alt((single_pane, horiz_split, vert_split))(input) -} - -fn container(input: &str) -> IResult<&str, Container> { - let (input, (dimensions, _, coordinates, element)) = - tuple((dimensions, char(','), coordinates, element))(input)?; - Ok(( - input, - Container { - dimensions, - coordinates, - element, - }, - )) -} - -#[cfg(test)] -mod tests { - - use super::{ - coordinates, dimensions, layout_id, single_pane, vert_split, window_layout, Container, - Coordinates, Dimensions, Element, Split, WindowLayout, - }; - - #[test] - fn test_parse_layout_id() { - let input = "9f58"; - - let actual = layout_id(input); - let expected = Ok(("", 40792_u16)); - assert_eq!(actual, expected); - } - - #[test] - fn test_parse_dimensions() { - let input = "237x0"; - - let actual = dimensions(input); - let expected = Ok(( - "", - Dimensions { - width: 237, - height: 0, - }, - )); - assert_eq!(actual, expected); - - let input = "7x13"; - - let actual = dimensions(input); - let expected = Ok(( - "", - Dimensions { - width: 7, - height: 13, - }, - )); - assert_eq!(actual, expected); - } - - #[test] - fn test_parse_coordinates() { - let input = "120,0"; - - let actual = coordinates(input); - let expected = Ok(("", Coordinates { x: 120, y: 0 })); - assert_eq!(actual, expected); - } - - #[test] - fn test_single_pane() { - let input = ",46"; - - let actual = single_pane(input); - let expected = Ok(("", Element::Pane { pane_id: 46 })); - assert_eq!(actual, expected); - } - - #[test] - fn test_vertical_split() { - let input = "[279x47,0,0,82,279x23,0,48,83]"; - - let actual = vert_split(input); - let expected = Ok(( - "", - Element::Vertical(Split { - elements: vec![ - Container { - dimensions: Dimensions { - width: 279, - height: 47, - }, - coordinates: Coordinates { x: 0, y: 0 }, - element: Element::Pane { pane_id: 82 }, - }, - Container { - dimensions: Dimensions { - width: 279, - height: 23, - }, - coordinates: Coordinates { x: 0, y: 48 }, - element: Element::Pane { pane_id: 83 }, - }, - ], - }), - )); - assert_eq!(actual, expected); - } - - #[test] - fn test_layout() { - let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]"; - - let actual = window_layout(input); - let expected = Ok(( - "", - WindowLayout { - id: 0x41e9, - container: Container { - dimensions: Dimensions { - width: 279, - height: 71, - }, - coordinates: Coordinates { x: 0, y: 0 }, - element: Element::Vertical(Split { - elements: vec![ - Container { - dimensions: Dimensions { - width: 279, - height: 40, - }, - coordinates: Coordinates { x: 0, y: 0 }, - element: Element::Pane { pane_id: 71 }, - }, - Container { - dimensions: Dimensions { - width: 279, - height: 30, - }, - coordinates: Coordinates { x: 0, y: 41 }, - element: Element::Horizontal(Split { - elements: vec![ - Container { - dimensions: Dimensions { - width: 147, - height: 30, - }, - coordinates: Coordinates { x: 0, y: 41 }, - element: Element::Pane { pane_id: 72 }, - }, - Container { - dimensions: Dimensions { - width: 131, - height: 30, - }, - coordinates: Coordinates { x: 148, y: 41 }, - element: Element::Pane { pane_id: 73 }, - }, - ], - }), - }, - ], - }), - }, - }, - )); - assert_eq!(actual, expected); - } - - #[test] - fn test_pane_ids() { - let input = "41e9,279x71,0,0[279x40,0,0,71,279x30,0,41{147x30,0,41,72,131x30,148,41,73}]"; - let (_, layout) = window_layout(input).unwrap(); - - let actual = layout.pane_ids(); - let expected = vec![71, 72, 73]; - assert_eq!(actual, expected); - } -} diff --git a/tmux-lib/src/lib.rs b/tmux-lib/src/lib.rs deleted file mode 100644 index bf8b3cd..0000000 --- a/tmux-lib/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Read or manipulate Tmux. -//! -//! Version requirement: _rustc 1.59.0+_ -//! -//! ```toml -//! [dependencies] -//! tmux-lib = "0.2" -//! ``` -//! -//! ## Getting started -//! -//! Work in progress -//! -//! ## Caveats -//! -//! - This is a beta version -//! -//! ## License -//! -//! Licensed under either of -//! -//! - [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) -//! - [MIT license](http://opensource.org/licenses/MIT) -//! -//! at your option. -//! -//! ### Contribution -//! -//! Unless you explicitly state otherwise, any contribution intentionally submitted -//! for inclusion in the work by you, as defined in the Apache-2.0 license, shall -//! be dual licensed as above, without any additional terms or conditions. - -pub mod error; - -pub mod client; -pub use client::display_message; -pub mod layout; -pub mod pane; -pub mod pane_id; -pub(crate) mod parse; -pub mod server; -pub mod session; -pub mod session_id; -pub mod utils; -pub mod window; -pub mod window_id; - -/// Result type for this crate. -pub type Result = std::result::Result; diff --git a/tmux-lib/src/pane.rs b/tmux-lib/src/pane.rs deleted file mode 100644 index 81cdcf9..0000000 --- a/tmux-lib/src/pane.rs +++ /dev/null @@ -1,255 +0,0 @@ -//! This module provides a few types and functions to handle Tmux Panes. -//! -//! The main use cases are running Tmux commands & parsing Tmux panes -//! information. - -use std::path::PathBuf; -use std::str::FromStr; - -use async_std::process::Command; -use nom::{ - character::complete::{char, digit1, not_line_ending}, - combinator::{all_consuming, map_res}, - sequence::tuple, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - error::{check_empty_process_output, map_add_intent, Error}, - pane_id::{parse::pane_id, PaneId}, - parse::{boolean, quoted_nonempty_string}, - window_id::WindowId, - Result, -}; - -/// A Tmux pane. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Pane { - /// Pane identifier, e.g. `%37`. - pub id: PaneId, - /// Describes the Pane index in the Window - pub index: u16, - /// Describes if the pane is currently active (focused). - pub is_active: bool, - /// Title of the Pane (usually defaults to the hostname) - pub title: String, - /// Current dirpath of the Pane - pub dirpath: PathBuf, - /// Current command executed in the Pane - pub command: String, -} - -impl FromStr for Pane { - type Err = Error; - - /// Parse a string containing tmux panes status into a new `Pane`. - /// - /// This returns a `Result` as this call can obviously - /// fail if provided an invalid format. - /// - /// The expected format of the tmux status is - /// - /// ```text - /// %20:0:false:'rmbp':'nvim':/Users/graelo/code/rust/tmux-backup - /// %21:1:true:'rmbp':'tmux':/Users/graelo/code/rust/tmux-backup - /// %27:2:false:'rmbp':'man man':/Users/graelo/code/rust/tmux-backup - /// ``` - /// - /// This status line is obtained with - /// - /// ```text - /// tmux list-panes -F "#{pane_id}:#{pane_index}:#{?pane_active,true,false}:'#{pane_title}':'#{pane_current_command}':#{pane_current_path}" - /// ``` - /// - /// For definitions, look at `Pane` type and the tmux man page for - /// definitions. - fn from_str(input: &str) -> std::result::Result { - 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)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(pane) - } -} - -impl Pane { - /// Return the entire Pane content as a `Vec`. - /// - /// # Note - /// - /// The output contains the escape codes, joined lines with trailing spaces. This output is - /// processed by the function `tmux_lib::utils::cleanup_captured_buffer`. - /// - pub async fn capture(&self) -> Result> { - let args = vec![ - "capture-pane", - "-t", - self.id.as_str(), - "-J", // preserves trailing spaces & joins any wrapped lines - "-e", // include escape sequences for text & background - "-p", // output goes to stdout - "-S", // starting line number - "-", // start of history - "-E", // ending line number - "-", // end of history - ]; - - 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)) = - tuple(( - pane_id, - char(':'), - map_res(digit1, str::parse), - char(':'), - boolean, - char(':'), - quoted_nonempty_string, - char(':'), - quoted_nonempty_string, - char(':'), - not_line_ending, - ))(input)?; - - Ok(( - input, - Pane { - id, - index, - is_active, - title: title.into(), - dirpath: dirpath.into(), - command: command.into(), - }, - )) - } -} - -// ------------------------------ -// Ops -// ------------------------------ - -/// Return a list of all `Pane` from all sessions. -pub async fn available_panes() -> Result> { - 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)?; - - // Each call to `Pane::parse` returns a `Result`. All results - // are collected into a Result, _>, thanks to `collect()`. - let result: Result> = buffer - .trim_end() // trim last '\n' as it would create an empty line - .split('\n') - .map(Pane::from_str) - .collect(); - - result -} - -/// Create a new pane (horizontal split) in the window with `window_id`, and return the new -/// pane id. -pub async fn new_pane( - reference_pane: &Pane, - pane_command: Option<&str>, - window_id: &WindowId, -) -> Result { - 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?; - let buffer = String::from_utf8(output.stdout)?; - - let new_id = PaneId::from_str(buffer.trim_end())?; - Ok(new_id) -} - -/// Select (make active) the pane with `pane_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 = vec![ - "%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> = 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); - } -} diff --git a/tmux-lib/src/pane_id.rs b/tmux-lib/src/pane_id.rs deleted file mode 100644 index ad51d57..0000000 --- a/tmux-lib/src/pane_id.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Pane id. - -use std::fmt; -use std::str::FromStr; - -use nom::{ - character::complete::{char, digit1}, - combinator::all_consuming, - sequence::preceded, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::error::{map_add_intent, Error}; - -/// The id of a Tmux pane. -/// -/// This wraps the raw tmux representation (`%12`). -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PaneId(pub String); - -impl FromStr for PaneId { - type Err = Error; - - /// Parse into `PaneId`. The `&str` must start with '%' followed by a `u32`. - fn from_str(input: &str) -> Result { - let desc = "PaneId"; - let intent = "#{pane_id}"; - - let (_, pane_id) = - all_consuming(parse::pane_id)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(pane_id) - } -} - -impl From<&u16> for PaneId { - fn from(value: &u16) -> Self { - Self(format!("%{value}")) - } -} - -impl PaneId { - /// Extract a string slice containing the raw representation. - #[must_use] - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for PaneId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -pub(crate) mod parse { - use super::{char, digit1, preceded, IResult, PaneId}; - - pub(crate) fn pane_id(input: &str) -> IResult<&str, PaneId> { - let (input, digit) = preceded(char('%'), digit1)(input)?; - let id = format!("%{digit}"); - Ok((input, PaneId(id))) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_pane_id_fn() { - let actual = parse::pane_id("%43"); - let expected = Ok(("", PaneId("%43".into()))); - assert_eq!(actual, expected); - - let actual = parse::pane_id("%4"); - let expected = Ok(("", PaneId("%4".into()))); - assert_eq!(actual, expected); - } - - #[test] - fn test_parse_pane_id_struct() { - let actual = PaneId::from_str("%43"); - assert!(actual.is_ok()); - assert_eq!(actual.unwrap(), PaneId("%43".into())); - - let actual = PaneId::from_str("4:38"); - assert!(matches!( - actual, - Err(Error::ParseError { - desc: "PaneId", - intent: "#{pane_id}", - err: _ - }) - )); - } -} diff --git a/tmux-lib/src/parse.rs b/tmux-lib/src/parse.rs deleted file mode 100644 index e8e2691..0000000 --- a/tmux-lib/src/parse.rs +++ /dev/null @@ -1,61 +0,0 @@ -use nom::{ - branch::alt, - bytes::complete::{escaped, tag}, - character::complete::none_of, - combinator::value, - sequence::delimited, - IResult, -}; - -/// Return the `&str` between single quotes. The returned string may be empty. -#[allow(unused)] -pub(crate) fn quoted_string(input: &str) -> IResult<&str, &str> { - let esc = escaped(none_of("\\\'"), '\\', tag("'")); - let esc_or_empty = alt((esc, tag(""))); - - delimited(tag("'"), esc_or_empty, tag("'"))(input) -} - -/// Return the `&str` between single quotes. The returned string may not be empty. -pub(crate) fn quoted_nonempty_string(input: &str) -> IResult<&str, &str> { - let esc = escaped(none_of("\\\'"), '\\', tag("'")); - delimited(tag("'"), esc, tag("'"))(input) -} - -/// Return a bool: allowed values: `"true"` or `"false"`. -pub(crate) fn boolean(input: &str) -> IResult<&str, bool> { - // This is a parser that returns `true` if it sees the string "true", and - // an error otherwise. - let parse_true = value(true, tag("true")); - - let parse_false = value(false, tag("false")); - - alt((parse_true, parse_false))(input) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_quoted_nonempty_string() { - let (input, res) = quoted_nonempty_string(r#"'foo\' 🤖 bar'"#).unwrap(); - assert!(input.is_empty()); - assert_eq!(res, r#"foo\' 🤖 bar"#); - let (input, res) = quoted_nonempty_string("'λx → x'").unwrap(); - assert!(input.is_empty()); - assert_eq!(res, "λx → x"); - let (input, res) = quoted_nonempty_string("' '").unwrap(); - assert!(input.is_empty()); - assert_eq!(res, " "); - - assert!(quoted_nonempty_string("''").is_err()); - } - - #[test] - fn test_quoted_string() { - let (input, res) = quoted_string("''").unwrap(); - assert!(input.is_empty()); - assert!(res.is_empty()); - } -} diff --git a/tmux-lib/src/server.rs b/tmux-lib/src/server.rs deleted file mode 100644 index b066eaa..0000000 --- a/tmux-lib/src/server.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Server management. - -use std::collections::HashMap; - -use async_std::process::Command; - -use crate::{ - error::{check_empty_process_output, Error}, - Result, -}; - -// ------------------------------ -// Ops -// ------------------------------ - -/// Start the Tmux server if needed, creating a session named `"[placeholder]"` in order to keep the server -/// running. -/// -/// It is ok-ish to already have an existing session named `"[placeholder]"`. -pub async fn start(initial_session_name: &str) -> Result<()> { - let args = vec!["new-session", "-d", "-s", initial_session_name]; - - let output = Command::new("tmux").args(&args).output().await?; - check_empty_process_output(&output, "new-session") -} - -/// Remove the session named `"[placeholder]"` used to keep the server alive. -pub async fn kill_session(name: &str) -> Result<()> { - let exact_name = format!("={name}"); - let args = vec!["kill-session", "-t", &exact_name]; - - let output = Command::new("tmux").args(&args).output().await?; - check_empty_process_output(&output, "kill-session") -} - -/// Return the value of a Tmux option. For instance, this can be used to get Tmux's default -/// command. -pub async fn show_option(option_name: &str, global: bool) -> Result> { - let mut args = vec!["show-options", "-w", "-q"]; - if global { - args.push("-g"); - } - args.push(option_name); - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - let buffer = buffer.trim_end(); - - if buffer.is_empty() { - return Ok(None); - } - Ok(Some(buffer.to_string())) -} - -/// Return all Tmux options as a `std::haosh::HashMap`. -pub async fn show_options(global: bool) -> Result> { - let args = if global { - vec!["show-options", "-g"] - } else { - vec!["show-options"] - }; - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - let pairs: HashMap = buffer - .trim_end() - .split('\n') - .into_iter() - .map(|s| s.split_at(s.find(' ').unwrap())) - .map(|(k, v)| (k.to_string(), v[1..].to_string())) - .collect(); - - Ok(pairs) -} - -/// Return the `"default-command"` used to start a pane, falling back to `"default shell"` if none. -/// -/// In case of bash, a `-l` flag is added. -pub async fn default_command() -> Result { - let all_options = show_options(true).await?; - - let default_shell = all_options - .get("default-shell") - .ok_or(Error::TmuxConfig("no default-shell")) - .map(|cmd| cmd.to_owned()) - .map(|cmd| { - if cmd.ends_with("bash") { - format!("-l {cmd}") - } else { - cmd - } - })?; - - all_options - .get("default-command") - .or(Some(&default_shell)) - .ok_or(Error::TmuxConfig("no default-command nor default-shell")) - .map(|cmd| cmd.to_owned()) -} diff --git a/tmux-lib/src/session.rs b/tmux-lib/src/session.rs deleted file mode 100644 index a205bab..0000000 --- a/tmux-lib/src/session.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! 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::{path::PathBuf, str::FromStr}; - -use async_std::process::Command; -use nom::{ - character::complete::{char, not_line_ending}, - combinator::all_consuming, - sequence::tuple, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - error::{map_add_intent, Error}, - pane::Pane, - pane_id::{parse::pane_id, PaneId}, - parse::quoted_nonempty_string, - session_id::{parse::session_id, SessionId}, - window::Window, - window_id::{parse::window_id, WindowId}, - Result, -}; - -/// A Tmux session. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Session { - /// Session identifier, e.g. `$3`. - pub id: SessionId, - /// Name of the session. - pub name: String, - /// Working directory of the session. - pub dirpath: PathBuf, -} - -impl FromStr for Session { - type Err = Error; - - /// 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':/Users/graelo/dl/pytorch - /// $2:'rust':/Users/graelo/rust - /// $3:'server: $~':/Users/graelo/swift - /// $4:'tmux-hacking':/Users/graelo/tmux - /// ``` - /// - /// This status line is obtained with - /// - /// ```text - /// tmux list-sessions -F "#{session_id}:'#{session_name}':#{session_path}" - /// ``` - /// - /// For definitions, look at `Session` type and the tmux man page for - /// definitions. - fn from_str(input: &str) -> std::result::Result { - let desc = "Session"; - let intent = "#{session_id}:'#{session_name}':#{session_path}"; - - let (_, sess) = - all_consuming(parse::session)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(sess) - } -} - -pub(crate) mod parse { - use super::*; - - pub(crate) fn session(input: &str) -> IResult<&str, Session> { - let (input, (id, _, name, _, dirpath)) = tuple(( - session_id, - char(':'), - quoted_nonempty_string, - char(':'), - not_line_ending, - ))(input)?; - - Ok(( - input, - Session { - id, - name: name.to_string(), - dirpath: dirpath.into(), - }, - )) - } -} - -// ------------------------------ -// Ops -// ------------------------------ - -/// Return a list of all `Session` from the current tmux session. -pub async fn available_sessions() -> Result> { - let args = vec![ - "list-sessions", - "-F", - "#{session_id}:'#{session_name}':#{session_path}", - ]; - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - - // Each call to `Session::parse` returns a `Result`. All results - // are collected into a Result, _>, thanks to `collect()`. - let result: Result> = buffer - .trim_end() // trim last '\n' as it would create an empty line - .split('\n') - .map(Session::from_str) - .collect(); - - result -} - -/// Create a Tmux session (and thus a window & pane). -/// -/// The new session attributes: -/// -/// - the session name is taken from the passed `session` -/// - the working directory is taken from the pane's working directory. -/// -pub async fn new_session( - session: &Session, - window: &Window, - pane: &Pane, - pane_command: Option<&str>, -) -> Result<(SessionId, WindowId, PaneId)> { - let mut args = vec![ - "new-session", - "-d", - "-c", - pane.dirpath.to_str().unwrap(), - "-s", - &session.name, - "-n", - &window.name, - "-P", - "-F", - "#{session_id}:#{window_id}:#{pane_id}", - ]; - if let Some(pane_command) = pane_command { - args.push(pane_command); - } - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - let buffer = buffer.trim_end(); - - let desc = "new-session"; - let intent = "#{session_id}:#{window_id}:#{pane_id}"; - let (_, (new_session_id, _, new_window_id, _, new_pane_id)) = all_consuming(tuple(( - session_id, - char(':'), - window_id, - char(':'), - pane_id, - )))(buffer) - .map_err(|e| map_add_intent(desc, intent, e))?; - - Ok((new_session_id, new_window_id, new_pane_id)) -} - -#[cfg(test)] -mod tests { - use super::Session; - use super::SessionId; - use crate::Result; - use std::path::PathBuf; - use std::str::FromStr; - - #[test] - fn parse_list_sessions() { - let output = vec![ - "$1:'pytorch':/Users/graelo/ml/pytorch", - "$2:'rust':/Users/graelo/rust", - "$3:'server: $':/Users/graelo/swift", - "$4:'tmux-hacking':/Users/graelo/tmux", - ]; - let sessions: Result> = - 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"), - dirpath: PathBuf::from("/Users/graelo/ml/pytorch"), - }, - Session { - id: SessionId::from_str("$2").unwrap(), - name: String::from("rust"), - dirpath: PathBuf::from("/Users/graelo/rust"), - }, - Session { - id: SessionId::from_str("$3").unwrap(), - name: String::from("server: $"), - dirpath: PathBuf::from("/Users/graelo/swift"), - }, - Session { - id: SessionId::from_str("$4").unwrap(), - name: String::from("tmux-hacking"), - dirpath: PathBuf::from("/Users/graelo/tmux"), - }, - ]; - - assert_eq!(sessions, expected); - } -} diff --git a/tmux-lib/src/session_id.rs b/tmux-lib/src/session_id.rs deleted file mode 100644 index a076032..0000000 --- a/tmux-lib/src/session_id.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Session Id. - -use std::str::FromStr; - -use nom::{ - character::complete::{char, digit1}, - combinator::all_consuming, - sequence::preceded, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::error::{map_add_intent, Error}; - -/// The id of a Tmux session. -/// -/// This wraps the raw tmux representation (`$11`). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SessionId(String); - -impl FromStr for SessionId { - type Err = Error; - - /// Parse into SessionId. The `&str` must start with '$' followed by a - /// `u16`. - fn from_str(input: &str) -> std::result::Result { - let desc = "SessionId"; - let intent = "#{session_id}"; - - let (_, sess_id) = - all_consuming(parse::session_id)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(sess_id) - } -} - -// impl SessionId { -// pub fn as_str(&self) -> &str { -// &self.0 -// } -// } - -pub(crate) mod parse { - use super::*; - - pub fn session_id(input: &str) -> IResult<&str, SessionId> { - let (input, digit) = preceded(char('$'), digit1)(input)?; - let id = format!("${digit}"); - Ok((input, SessionId(id))) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_session_id_fn() { - let actual = parse::session_id("$43"); - let expected = Ok(("", SessionId("$43".into()))); - assert_eq!(actual, expected); - - let actual = parse::session_id("$4"); - let expected = Ok(("", SessionId("$4".into()))); - assert_eq!(actual, expected); - } - - #[test] - fn test_parse_session_id_struct() { - let actual = SessionId::from_str("$43"); - assert!(actual.is_ok()); - assert_eq!(actual.unwrap(), SessionId("$43".into())); - - let actual = SessionId::from_str("4:38"); - assert!(matches!( - actual, - Err(Error::ParseError { - desc: "SessionId", - intent: "#{session_id}", - err: _ - }) - )); - } -} diff --git a/tmux-lib/src/utils.rs b/tmux-lib/src/utils.rs deleted file mode 100644 index 0e9f42c..0000000 --- a/tmux-lib/src/utils.rs +++ /dev/null @@ -1,140 +0,0 @@ -/// Misc utilities. - -pub(crate) trait SliceExt { - fn trim(&self) -> &Self; - fn trim_trailing(&self) -> &Self; -} - -fn is_whitespace(c: &u8) -> bool { - *c == b'\t' || *c == b' ' -} - -fn is_not_whitespace(c: &u8) -> bool { - !is_whitespace(c) -} - -impl SliceExt for [u8] { - /// Trim leading and trailing whitespaces (`\t` and ` `) in a `&[u8]` - fn trim(&self) -> &[u8] { - if let Some(first) = self.iter().position(is_not_whitespace) { - if let Some(last) = self.iter().rposition(is_not_whitespace) { - &self[first..last + 1] - } else { - unreachable!(); - } - } else { - &[] - } - } - - /// Trim trailing whitespaces (`\t` and ` `) in a `&[u8]` - fn trim_trailing(&self) -> &[u8] { - if let Some(last) = self.iter().rposition(is_not_whitespace) { - &self[0..last + 1] - } else { - &[] - } - } -} - -/// Trim each line of the buffer. -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. -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() - } -} - -/// This function processes a pane captured bufer. -/// -/// - All lines are trimmed after capture because tmux does not allow capturing escape codes and -/// trimming lines. -/// - If `drop_n_last_lines` is greater than 0, the n last line are not captured. This is used only -/// for panes with a zsh prompt, in order to avoid polluting the history with new prompts on -/// restore. -/// - In addition, the last line has an additional ascii reset escape code because tmux does not -/// capture it. -/// -pub fn cleanup_captured_buffer(buffer: &[u8], drop_n_last_lines: usize) -> Vec { - let trimmed_lines: Vec<&[u8]> = buf_trim_trailing(buffer); - let mut buffer: Vec<&[u8]> = 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 final_buffer: Vec = Vec::with_capacity(buffer.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(); - final_buffer.extend_from_slice(reset); - final_buffer.push(b'\n'); - } else { - final_buffer.push(b'\n'); - } - } - - final_buffer -} - -#[cfg(test)] -mod tests { - use super::{buf_trim_trailing, drop_last_empty_lines, SliceExt}; - - #[test] - fn trims_trailing_whitespaces() { - let input = " text ".as_bytes(); - let expected = " text".as_bytes(); - - let actual = input.trim_trailing(); - assert_eq!(actual, expected); - } - - #[test] - fn trims_whitespaces() { - let input = " text ".as_bytes(); - let expected = "text".as_bytes(); - - let actual = input.trim(); - assert_eq!(actual, expected); - } - - #[test] - fn test_buf_trim_trailing() { - let text = "line1\n\nline3 "; - let actual = 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 = buf_trim_trailing(text.as_bytes()); - let actual = drop_last_empty_lines(&trimmed_lines); - let expected = trimmed_lines; - assert_eq!(actual, expected); - - // - - let text = "line1\nline2\n\n\n "; - - let trimmed_lines = buf_trim_trailing(text.as_bytes()); - let actual = drop_last_empty_lines(&trimmed_lines); - let expected = vec!["line1".as_bytes(), "line2".as_bytes()]; - assert_eq!(actual, expected); - } -} diff --git a/tmux-lib/src/window.rs b/tmux-lib/src/window.rs deleted file mode 100644 index 2031de1..0000000 --- a/tmux-lib/src/window.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! 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::str::FromStr; - -use async_std::process::Command; - -use nom::{ - character::complete::{char, digit1}, - combinator::{all_consuming, map_res, recognize}, - sequence::tuple, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::{ - error::{check_empty_process_output, map_add_intent, Error}, - layout::{self, window_layout}, - pane::Pane, - pane_id::{parse::pane_id, PaneId}, - parse::{boolean, quoted_nonempty_string}, - session::Session, - window_id::{parse::window_id, WindowId}, - Result, -}; - -/// A Tmux window. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Window { - /// Window identifier, e.g. `@3`. - pub id: WindowId, - /// Index of the Window in the Session. - 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, -} - -impl FromStr for Window { - type Err = Error; - - /// 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':'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-backup':'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(input: &str) -> std::result::Result { - let desc = "Window"; - let intent = "#{window_id}:#{window_index}:#{?window_active,true,false}:#{window_layout}:'#{window_name}':'#{window_linked_sessions_list}'"; - - let (_, window) = - all_consuming(parse::window)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(window) - } -} - -impl Window { - /// Return all `PaneId` in this window. - pub fn pane_ids(&self) -> Vec { - let layout = layout::parse_window_layout(&self.layout).unwrap(); - layout.pane_ids().iter().map(PaneId::from).collect() - } -} - -pub(crate) mod parse { - use super::*; - - pub(crate) fn window(input: &str) -> IResult<&str, Window> { - let (input, (id, _, index, _, is_active, _, layout, _, name, _, session_names)) = - tuple(( - window_id, - char(':'), - map_res(digit1, str::parse), - char(':'), - boolean, - char(':'), - recognize(window_layout), - char(':'), - quoted_nonempty_string, - char(':'), - quoted_nonempty_string, - ))(input)?; - - Ok(( - input, - Window { - id, - index, - is_active, - layout: layout.to_string(), - name: name.to_string(), - sessions: vec![session_names.to_string()], - }, - )) - } -} - -// ------------------------------ -// Ops -// ------------------------------ - -/// Return a list of all `Window` from all sessions. -pub async fn available_windows() -> Result> { - 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 = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - - // Note: each call to the `Window::from_str` returns a `Result`. - // All results are then collected into a Result, _>, via - // `collect()`. - let result: Result> = buffer - .trim_end() // trim last '\n' as it would create an empty line - .split('\n') - .map(Window::from_str) - .collect(); - - result -} - -/// Create a Tmux window in a session exactly named as the passed `session`. -/// -/// The new window attributes: -/// -/// - created in the `session` -/// - the window name is taken from the passed `window` -/// - the working directory is the pane's working directory. -/// -pub async fn new_window( - session: &Session, - window: &Window, - pane: &Pane, - pane_command: Option<&str>, -) -> Result<(WindowId, PaneId)> { - let exact_session_name = format!("={}", session.name); - - let mut args = vec![ - "new-window", - "-d", - "-c", - pane.dirpath.to_str().unwrap(), - "-n", - &window.name, - "-t", - &exact_session_name, - "-P", - "-F", - "#{window_id}:#{pane_id}", - ]; - if let Some(pane_command) = pane_command { - args.push(pane_command); - } - - let output = Command::new("tmux").args(&args).output().await?; - let buffer = String::from_utf8(output.stdout)?; - let buffer = buffer.trim_end(); - - let desc = "new-window"; - let intent = "#{window_id}:#{pane_id}"; - - let (_, (new_window_id, _, new_pane_id)) = - all_consuming(tuple((window_id, char(':'), pane_id)))(buffer) - .map_err(|e| map_add_intent(desc, intent, e))?; - - Ok((new_window_id, new_pane_id)) -} - -/// Apply the provided `layout` to the window with `window_id`. -pub async fn set_layout(layout: &str, window_id: &WindowId) -> Result<()> { - let args = vec!["select-layout", "-t", window_id.as_str(), layout]; - - let output = Command::new("tmux").args(&args).output().await?; - check_empty_process_output(&output, "select-layout") -} - -/// Select (make active) the window with `window_id`. -pub async fn select_window(window_id: &WindowId) -> Result<()> { - let args = vec!["select-window", "-t", window_id.as_str()]; - - let output = Command::new("tmux").args(&args).output().await?; - check_empty_process_output(&output, "select-window") -} - -#[cfg(test)] -mod tests { - use super::Window; - use super::WindowId; - use crate::Result; - 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-backup':'tmux-hacking'", - ]; - let sessions: Result> = - 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-backup"), - sessions: vec![String::from("tmux-hacking")], - }, - ]; - - assert_eq!(windows, expected); - } -} diff --git a/tmux-lib/src/window_id.rs b/tmux-lib/src/window_id.rs deleted file mode 100644 index fa16c84..0000000 --- a/tmux-lib/src/window_id.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Window Id. - -use std::str::FromStr; - -use nom::{ - character::complete::{char, digit1}, - combinator::all_consuming, - sequence::preceded, - IResult, -}; -use serde::{Deserialize, Serialize}; - -use crate::error::{map_add_intent, Error}; - -/// The id of a Tmux window. -/// -/// This wraps the raw tmux representation (`@41`). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WindowId(String); - -impl FromStr for WindowId { - type Err = Error; - - /// Parse into WindowId. The `&str` must start with '@' followed by a - /// `u16`. - fn from_str(input: &str) -> std::result::Result { - let desc = "WindowId"; - let intent = "#{window_id}"; - - let (_, window_id) = - all_consuming(parse::window_id)(input).map_err(|e| map_add_intent(desc, intent, e))?; - - Ok(window_id) - } -} - -impl WindowId { - /// Extract a string slice containing the raw representation. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -pub(crate) mod parse { - use super::*; - - pub(crate) fn window_id(input: &str) -> IResult<&str, WindowId> { - let (input, digit) = preceded(char('@'), digit1)(input)?; - let id = format!("@{digit}"); - Ok((input, WindowId(id))) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_window_id_fn() { - let actual = parse::window_id("@43"); - let expected = Ok(("", WindowId("@43".into()))); - assert_eq!(actual, expected); - - let actual = parse::window_id("@4"); - let expected = Ok(("", WindowId("@4".into()))); - assert_eq!(actual, expected); - } - - #[test] - fn test_parse_window_id_struct() { - let actual = WindowId::from_str("@43"); - assert!(actual.is_ok()); - assert_eq!(actual.unwrap(), WindowId("@43".into())); - - let actual = WindowId::from_str("4:38"); - assert!(matches!( - actual, - Err(Error::ParseError { - desc: "WindowId", - intent: "#{window_id}", - err: _ - }) - )); - } -}