From 2b229fc147f602e4b35087bc09a41a982616d358 Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 23 Aug 2021 19:59:46 +0800 Subject: [PATCH 01/21] parse tmux control mode events when in tmux mode, bypass vtparse --- termwiz/Cargo.toml | 1 + termwiz/src/escape/mod.rs | 5 ++++ termwiz/src/escape/parser/mod.rs | 43 ++++++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/termwiz/Cargo.toml b/termwiz/Cargo.toml index 61c6e821245..ccf0819b024 100644 --- a/termwiz/Cargo.toml +++ b/termwiz/Cargo.toml @@ -32,6 +32,7 @@ serde = {version="1.0", features = ["rc", "derive"], optional=true} sha2 = "0.9" terminfo = "0.7" thiserror = "1.0" +tmux-cc = {version = "0.1", path = "../tmux-cc"} unicode-segmentation = "1.8" unicode-width = "0.1" ucd-trie = "0.1" diff --git a/termwiz/src/escape/mod.rs b/termwiz/src/escape/mod.rs index e0293eab3ea..42bd4abe022 100644 --- a/termwiz/src/escape/mod.rs +++ b/termwiz/src/escape/mod.rs @@ -8,6 +8,7 @@ //! only; it does not provide terminal emulation facilities itself. use num_derive::*; use std::fmt::{Display, Error as FmtError, Formatter, Write as FmtWrite}; +use tmux_cc::Event; pub mod apc; pub mod csi; @@ -178,6 +179,8 @@ pub enum DeviceControlMode { Data(u8), /// A self contained (Enter, Data*, Exit) sequence ShortDeviceControl(Box), + /// Tmux parsed events + TmuxEvents(Box>), } impl Display for DeviceControlMode { @@ -201,6 +204,7 @@ impl Display for DeviceControlMode { Self::Exit => Ok(()), Self::Data(c) => f.write_char(*c as char), Self::ShortDeviceControl(s) => s.fmt(f), + Self::TmuxEvents(_) => write!(f, "tmux event"), } } } @@ -212,6 +216,7 @@ impl std::fmt::Debug for DeviceControlMode { Self::Exit => write!(fmt, "Exit"), Self::Data(b) => write!(fmt, "Data({:?} 0x{:x})", *b as char, *b), Self::ShortDeviceControl(s) => write!(fmt, "ShortDeviceControl({:?})", s), + Self::TmuxEvents(_) => write!(fmt, "tmux event"), } } } diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 28da945ecce..67703ec855a 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -7,7 +7,9 @@ use crate::escape::{ use log::error; use num_traits::FromPrimitive; use regex::bytes::Regex; -use std::cell::RefCell; +use std::borrow::{Borrow, BorrowMut}; +use std::cell::{Ref, RefCell}; +use tmux_cc; use vtparse::{CsiParam, VTActor, VTParser}; struct SixelBuilder { @@ -53,6 +55,7 @@ struct ParseState { sixel: Option, dcs: Option, get_tcap: Option, + tmux_state: Option>, } /// The `Parser` struct holds the state machine that is used to decode @@ -81,11 +84,23 @@ impl Parser { } pub fn parse(&mut self, bytes: &[u8], mut callback: F) { - let mut perform = Performer { - callback: &mut callback, - state: &mut self.state.borrow_mut(), - }; - self.state_machine.parse(bytes, &mut perform); + let tmux_state: bool = self.state.borrow().tmux_state.is_some(); + if tmux_state { + let parser_state = self.state.borrow(); + let tmux_state = parser_state.tmux_state.as_ref().unwrap(); + let mut tmux_parser = tmux_state.borrow_mut(); + let tmux_events = tmux_parser.advance_bytes(bytes); + log::info!("parsed tmux events: {:?}", &tmux_events); + callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(tmux_events), + ))); + } else { + let mut perform = Performer { + callback: &mut callback, + state: &mut self.state.borrow_mut(), + }; + self.state_machine.parse(bytes, &mut perform); + } } /// A specialized version of the parser that halts after recognizing the @@ -215,6 +230,10 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> { data: vec![], }); } else { + if byte == b'p' && params == [1000] { + // into tmux_cc mode + self.state.borrow_mut().tmux_state = Some(RefCell::new(tmux_cc::Parser::new())); + } (self.callback)(Action::DeviceControl(DeviceControlMode::Enter(Box::new( EnterDeviceControlMode { byte, @@ -234,7 +253,17 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> { } else if let Some(tcap) = self.state.get_tcap.as_mut() { tcap.push(data); } else { - (self.callback)(Action::DeviceControl(DeviceControlMode::Data(data))); + if let Some(tmux_state) = &self.state.tmux_state { + let mut tmux_parser = tmux_state.borrow_mut(); + if let Some(tmux_event) = tmux_parser.advance_byte(data) { + log::info!("parsed tmux event:{:?}", &tmux_event); + (self.callback)(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(vec![tmux_event]), + ))); + } + } else { + (self.callback)(Action::DeviceControl(DeviceControlMode::Data(data))); + } } } From b8e892443c6c3413354efed051ab9fbc2e61db56 Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 23 Aug 2021 20:00:31 +0800 Subject: [PATCH 02/21] handle tmux events DCS event --- mux/src/localpane.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/mux/src/localpane.rs b/mux/src/localpane.rs index 905bbe59dc3..3e6644f7687 100644 --- a/mux/src/localpane.rs +++ b/mux/src/localpane.rs @@ -531,14 +531,17 @@ impl wezterm_term::DeviceControlHandler for LocalPaneDCSHandler { } } DeviceControlMode::Data(c) => { + log::warn!( + "unhandled DeviceControlMode::Data {:x} {}", + c, + (c as char).escape_debug() + ); + } + DeviceControlMode::TmuxEvents(events) => { if let Some(tmux) = self.tmux_domain.as_ref() { - tmux.advance(c); + tmux.advance(events); } else { - log::warn!( - "unhandled DeviceControlMode::Data {:x} {}", - c, - (c as char).escape_debug() - ); + log::warn!("unhandled DeviceControlMode::TmuxEvents {:?}", &events); } } _ => { From 7604074932c4fc38ddda8560c7e1e80977d4c849 Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 23 Aug 2021 20:44:49 +0800 Subject: [PATCH 03/21] tmux domain handle events --- mux/Cargo.toml | 1 + mux/src/tmux.rs | 56 +++++++++++++++++++++++++++++++++------- mux/src/tmux_commands.rs | 0 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 mux/src/tmux_commands.rs diff --git a/mux/Cargo.toml b/mux/Cargo.toml index 9ebf39dbeda..8585c033140 100644 --- a/mux/Cargo.toml +++ b/mux/Cargo.toml @@ -36,6 +36,7 @@ unicode-segmentation = "1.8" url = "2" wezterm-ssh = { path = "../wezterm-ssh" } wezterm-term = { path = "../term", features=["use_serde"] } +flume = "0.10" [target.'cfg(any(windows, target_os="linux", target_os="macos"))'.dependencies] sysinfo = "0.16" diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 100c0f5c089..7e96ec77d82 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -5,9 +5,10 @@ use crate::window::WindowId; use crate::Mux; use anyhow::anyhow; use async_trait::async_trait; +use flume; use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use std::sync::Arc; use tmux_cc::*; @@ -112,12 +113,26 @@ impl TmuxCommand for ListAllPanes { } } +struct TmuxRemotePane { + // members for local + local_pane_id: PaneId, + tx: flume::Sender, + // members sync with remote + session_id: TmuxSessionId, + window_id: TmuxWindowId, + pane_id: TmuxPaneId, +} + pub(crate) struct TmuxDomainState { pane_id: PaneId, pub domain_id: DomainId, - parser: RefCell, + // parser: RefCell, state: RefCell, cmd_queue: RefCell>>, + gui_window_id: RefCell>, + remote_panes: RefCell>, + // TODO: Currently, a tmux pane is a local tab. + // add info about window-pane mapping to support this feature. } pub struct TmuxDomain { @@ -125,13 +140,12 @@ pub struct TmuxDomain { } impl TmuxDomainState { - pub fn advance(&self, b: u8) { - let mut parser = self.parser.borrow_mut(); - if let Some(event) = parser.advance_byte(b) { + pub fn advance(&self, events: Box>) { + for event in events.iter() { let state = *self.state.borrow(); log::error!("tmux: {:?} in state {:?}", event, state); - if let Event::Guarded(response) = event { - match state { + match event { + Event::Guarded(response) => match state { State::WaitForInitialGuard => { *self.state.borrow_mut() = State::Idle; } @@ -139,17 +153,37 @@ impl TmuxDomainState { let cmd = self.cmd_queue.borrow_mut().pop_front().unwrap(); let domain_id = self.domain_id; *self.state.borrow_mut() = State::Idle; + let resp = response.clone(); promise::spawn::spawn(async move { - if let Err(err) = cmd.process_result(domain_id, &response) { + if let Err(err) = cmd.process_result(domain_id, &resp) { log::error!("error processing result: {}", err); } }) .detach(); } State::Idle => {} + }, + Event::Output { pane, text } => { + // TODO: write to pane's tmux queue + // TODO: decode string + } + Event::WindowAdd { window } => { + if self.gui_window_id.borrow().is_none() { + if let Some(mux) = Mux::get() { + let window_builder = mux.new_empty_window(); + log::info!("Tmux create window id {}", window_builder.window_id); + { + let mut window_id = self.gui_window_id.borrow_mut(); + *window_id = Some(window_builder.window_id); + } + } + } } + _ => {} } } + + // send pending commands to tmux if *self.state.borrow() == State::Idle && !self.cmd_queue.borrow().is_empty() { let domain_id = self.domain_id; promise::spawn::spawn(async move { @@ -184,15 +218,17 @@ impl TmuxDomainState { impl TmuxDomain { pub fn new(pane_id: PaneId) -> Self { let domain_id = alloc_domain_id(); - let parser = RefCell::new(Parser::new()); + // let parser = RefCell::new(Parser::new()); let mut cmd_queue = VecDeque::>::new(); cmd_queue.push_back(Box::new(ListAllPanes)); let inner = Arc::new(TmuxDomainState { domain_id, pane_id, - parser, + // parser, state: RefCell::new(State::WaitForInitialGuard), cmd_queue: RefCell::new(cmd_queue), + gui_window_id: RefCell::new(None), + remote_panes: RefCell::new(HashMap::default()), }); Self { inner } } diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs new file mode 100644 index 00000000000..e69de29bb2d From 24c8b1ec866493735b6f9069d210e7fedb0e6fde Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 23 Aug 2021 20:48:09 +0800 Subject: [PATCH 04/21] split out tmux commands --- mux/src/lib.rs | 1 + mux/src/tmux.rs | 95 +--------------------------------------- mux/src/tmux_commands.rs | 94 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 94 deletions(-) diff --git a/mux/src/lib.rs b/mux/src/lib.rs index 73c58bf8e54..989c8d11c9c 100644 --- a/mux/src/lib.rs +++ b/mux/src/lib.rs @@ -34,6 +34,7 @@ pub mod ssh; pub mod tab; pub mod termwiztermtab; pub mod tmux; +pub mod tmux_commands; pub mod window; use crate::activity::Activity; diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 7e96ec77d82..e5ce2f37f71 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -1,9 +1,9 @@ use crate::domain::{alloc_domain_id, Domain, DomainId, DomainState}; use crate::pane::{Pane, PaneId}; use crate::tab::{SplitDirection, Tab, TabId}; +use crate::tmux_commands::{ListAllPanes, TmuxCommand}; use crate::window::WindowId; use crate::Mux; -use anyhow::anyhow; use async_trait::async_trait; use flume; use portable_pty::{CommandBuilder, PtySize}; @@ -20,99 +20,6 @@ enum State { WaitingForResponse, } -trait TmuxCommand { - fn get_command(&self) -> String; - fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; -} - -struct ListAllPanes; -impl TmuxCommand for ListAllPanes { - fn get_command(&self) -> String { - "list-panes -aF '#{session_id} #{window_id} #{pane_id} \ - #{pane_index} #{cursor_x} #{cursor_y} #{pane_width} #{pane_height} \ - #{pane_left} #{pane_top}'\n" - .to_owned() - } - - fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()> { - #[derive(Debug)] - struct Item { - session_id: TmuxSessionId, - window_id: TmuxWindowId, - pane_id: TmuxPaneId, - pane_index: u64, - cursor_x: u64, - cursor_y: u64, - pane_width: u64, - pane_height: u64, - pane_left: u64, - pane_top: u64, - } - - let mut items = vec![]; - - for line in result.output.split('\n') { - if line.is_empty() { - continue; - } - let mut fields = line.split(' '); - let session_id = fields.next().ok_or_else(|| anyhow!("missing session_id"))?; - let window_id = fields.next().ok_or_else(|| anyhow!("missing window_id"))?; - let pane_id = fields.next().ok_or_else(|| anyhow!("missing pane_id"))?; - let pane_index = fields - .next() - .ok_or_else(|| anyhow!("missing pane_index"))? - .parse()?; - let cursor_x = fields - .next() - .ok_or_else(|| anyhow!("missing cursor_x"))? - .parse()?; - let cursor_y = fields - .next() - .ok_or_else(|| anyhow!("missing cursor_y"))? - .parse()?; - let pane_width = fields - .next() - .ok_or_else(|| anyhow!("missing pane_width"))? - .parse()?; - let pane_height = fields - .next() - .ok_or_else(|| anyhow!("missing pane_height"))? - .parse()?; - let pane_left = fields - .next() - .ok_or_else(|| anyhow!("missing pane_left"))? - .parse()?; - let pane_top = fields - .next() - .ok_or_else(|| anyhow!("missing pane_top"))? - .parse()?; - - // These ids all have various sigils such as `$`, `%`, `@`, - // so skip those prior to parsing them - let session_id = session_id[1..].parse()?; - let window_id = window_id[1..].parse()?; - let pane_id = pane_id[1..].parse()?; - - items.push(Item { - session_id, - window_id, - pane_id, - pane_index, - cursor_x, - cursor_y, - pane_width, - pane_height, - pane_left, - pane_top, - }); - } - - log::error!("panes in domain_id {}: {:?}", domain_id, items); - Ok(()) - } -} - struct TmuxRemotePane { // members for local local_pane_id: PaneId, diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index e69de29bb2d..1014090e288 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -0,0 +1,94 @@ +use anyhow::anyhow; + +pub(crate) trait TmuxCommand { + fn get_command(&self) -> String; + fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; +} + +pub(crate) struct ListAllPanes; +impl TmuxCommand for ListAllPanes { + fn get_command(&self) -> String { + "list-panes -aF '#{session_id} #{window_id} #{pane_id} \ + #{pane_index} #{cursor_x} #{cursor_y} #{pane_width} #{pane_height} \ + #{pane_left} #{pane_top}'\n" + .to_owned() + } + + fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()> { + #[derive(Debug)] + struct Item { + session_id: TmuxSessionId, + window_id: TmuxWindowId, + pane_id: TmuxPaneId, + pane_index: u64, + cursor_x: u64, + cursor_y: u64, + pane_width: u64, + pane_height: u64, + pane_left: u64, + pane_top: u64, + } + + let mut items = vec![]; + + for line in result.output.split('\n') { + if line.is_empty() { + continue; + } + let mut fields = line.split(' '); + let session_id = fields.next().ok_or_else(|| anyhow!("missing session_id"))?; + let window_id = fields.next().ok_or_else(|| anyhow!("missing window_id"))?; + let pane_id = fields.next().ok_or_else(|| anyhow!("missing pane_id"))?; + let pane_index = fields + .next() + .ok_or_else(|| anyhow!("missing pane_index"))? + .parse()?; + let cursor_x = fields + .next() + .ok_or_else(|| anyhow!("missing cursor_x"))? + .parse()?; + let cursor_y = fields + .next() + .ok_or_else(|| anyhow!("missing cursor_y"))? + .parse()?; + let pane_width = fields + .next() + .ok_or_else(|| anyhow!("missing pane_width"))? + .parse()?; + let pane_height = fields + .next() + .ok_or_else(|| anyhow!("missing pane_height"))? + .parse()?; + let pane_left = fields + .next() + .ok_or_else(|| anyhow!("missing pane_left"))? + .parse()?; + let pane_top = fields + .next() + .ok_or_else(|| anyhow!("missing pane_top"))? + .parse()?; + + // These ids all have various sigils such as `$`, `%`, `@`, + // so skip those prior to parsing them + let session_id = session_id[1..].parse()?; + let window_id = window_id[1..].parse()?; + let pane_id = pane_id[1..].parse()?; + + items.push(Item { + session_id, + window_id, + pane_id, + pane_index, + cursor_x, + cursor_y, + pane_width, + pane_height, + pane_left, + pane_top, + }); + } + + log::error!("panes in domain_id {}: {:?}", domain_id, items); + Ok(()) + } +} From fdc386974c739e33e530738ae33376c05b90ce4b Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 25 Aug 2021 22:16:09 +0800 Subject: [PATCH 05/21] split out tmux command process --- Cargo.lock | 2 ++ mux/src/tmux.rs | 9 +++-- mux/src/tmux_commands.rs | 61 ++++++++++++++++++++++---------- termwiz/src/escape/parser/mod.rs | 1 + 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ded58d6d6a2..17fbf5e4f28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2529,6 +2529,7 @@ dependencies = [ "crossbeam", "downcast-rs", "filedescriptor", + "flume", "k9", "lazy_static", "libc", @@ -4358,6 +4359,7 @@ dependencies = [ "terminfo", "termios 0.3.3", "thiserror", + "tmux-cc", "ucd-trie", "unicode-segmentation", "unicode-width", diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index e5ce2f37f71..fb23d78af7e 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -1,7 +1,7 @@ use crate::domain::{alloc_domain_id, Domain, DomainId, DomainState}; use crate::pane::{Pane, PaneId}; use crate::tab::{SplitDirection, Tab, TabId}; -use crate::tmux_commands::{ListAllPanes, TmuxCommand}; +use crate::tmux_commands::{process_command_result, ListAllPanes, TmuxCommand}; use crate::window::WindowId; use crate::Mux; use async_trait::async_trait; @@ -62,8 +62,11 @@ impl TmuxDomainState { *self.state.borrow_mut() = State::Idle; let resp = response.clone(); promise::spawn::spawn(async move { - if let Err(err) = cmd.process_result(domain_id, &resp) { - log::error!("error processing result: {}", err); + match cmd.process_result(domain_id, &resp) { + Ok(result) => process_command_result(domain_id, result), + Err(err) => { + log::error!("error processing result: {}", err); + } } }) .detach(); diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 1014090e288..42195aa9920 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -1,8 +1,34 @@ +use crate::domain::DomainId; +use crate::tmux::TmuxDomain; +use crate::Mux; use anyhow::anyhow; +use tmux_cc::*; pub(crate) trait TmuxCommand { fn get_command(&self) -> String; - fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; + fn process_result( + &self, + domain_id: DomainId, + result: &Guarded, + ) -> anyhow::Result; +} + +#[derive(Debug)] +pub(crate) struct PaneItem { + session_id: TmuxSessionId, + window_id: TmuxWindowId, + pane_id: TmuxPaneId, + pane_index: u64, + cursor_x: u64, + cursor_y: u64, + pane_width: u64, + pane_height: u64, + pane_left: u64, + pane_top: u64, +} + +pub(crate) enum TmuxCommandResult { + PaneList(Vec), } pub(crate) struct ListAllPanes; @@ -14,21 +40,11 @@ impl TmuxCommand for ListAllPanes { .to_owned() } - fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()> { - #[derive(Debug)] - struct Item { - session_id: TmuxSessionId, - window_id: TmuxWindowId, - pane_id: TmuxPaneId, - pane_index: u64, - cursor_x: u64, - cursor_y: u64, - pane_width: u64, - pane_height: u64, - pane_left: u64, - pane_top: u64, - } - + fn process_result( + &self, + domain_id: DomainId, + result: &Guarded, + ) -> anyhow::Result { let mut items = vec![]; for line in result.output.split('\n') { @@ -74,7 +90,7 @@ impl TmuxCommand for ListAllPanes { let window_id = window_id[1..].parse()?; let pane_id = pane_id[1..].parse()?; - items.push(Item { + items.push(PaneItem { session_id, window_id, pane_id, @@ -89,6 +105,15 @@ impl TmuxCommand for ListAllPanes { } log::error!("panes in domain_id {}: {:?}", domain_id, items); - Ok(()) + Ok(TmuxCommandResult::PaneList(items)) + } +} + +pub(crate) fn process_command_result(domain_id: DomainId, result: TmuxCommandResult) { + let mux = Mux::get().expect("to be called on main thread"); + if let Some(domain) = mux.get_domain(domain_id) { + if let Some(tmux_domain) = domain.downcast_ref::() { + // TODO: + } } } diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 67703ec855a..767536a8db3 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -89,6 +89,7 @@ impl Parser { let parser_state = self.state.borrow(); let tmux_state = parser_state.tmux_state.as_ref().unwrap(); let mut tmux_parser = tmux_state.borrow_mut(); + // TODO: wrap events into some Result to capture bytes cannot be parsed let tmux_events = tmux_parser.advance_bytes(bytes); log::info!("parsed tmux events: {:?}", &tmux_events); callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( From fd36acf6adad0d2817e217234025004add57304a Mon Sep 17 00:00:00 2001 From: g4c Date: Thu, 26 Aug 2021 21:02:34 +0800 Subject: [PATCH 06/21] tmux pty --- mux/src/lib.rs | 1 + mux/src/tmux.rs | 35 ++++++++++++------- mux/src/tmux_commands.rs | 37 +++++++++----------- mux/src/tmux_pty.rs | 74 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 mux/src/tmux_pty.rs diff --git a/mux/src/lib.rs b/mux/src/lib.rs index 989c8d11c9c..6f8f8ca59af 100644 --- a/mux/src/lib.rs +++ b/mux/src/lib.rs @@ -35,6 +35,7 @@ pub mod tab; pub mod termwiztermtab; pub mod tmux; pub mod tmux_commands; +mod tmux_pty; pub mod window; use crate::activity::Activity; diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index fb23d78af7e..4749a0b0c65 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -1,7 +1,7 @@ use crate::domain::{alloc_domain_id, Domain, DomainId, DomainState}; use crate::pane::{Pane, PaneId}; use crate::tab::{SplitDirection, Tab, TabId}; -use crate::tmux_commands::{process_command_result, ListAllPanes, TmuxCommand}; +use crate::tmux_commands::{ListAllPanes, TmuxCommand}; use crate::window::WindowId; use crate::Mux; use async_trait::async_trait; @@ -10,7 +10,7 @@ use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use tmux_cc::*; #[derive(PartialEq, Eq, Debug, Copy, Clone)] @@ -20,7 +20,7 @@ enum State { WaitingForResponse, } -struct TmuxRemotePane { +pub(crate) struct TmuxRemotePane { // members for local local_pane_id: PaneId, tx: flume::Sender, @@ -28,6 +28,20 @@ struct TmuxRemotePane { session_id: TmuxSessionId, window_id: TmuxWindowId, pane_id: TmuxPaneId, + pub cursor_x: u64, + pub cursor_y: u64, + pub pane_width: u64, + pub pane_height: u64, + pub pane_left: u64, + pub pane_top: u64, +} + +pub(crate) type RefTmuxRemotePane = Arc>; + +pub(crate) struct TmuxTab { + tab_id: TabId, + tmux_window_id: TmuxWindowId, + panes: Vec, } pub(crate) struct TmuxDomainState { @@ -37,9 +51,8 @@ pub(crate) struct TmuxDomainState { state: RefCell, cmd_queue: RefCell>>, gui_window_id: RefCell>, - remote_panes: RefCell>, - // TODO: Currently, a tmux pane is a local tab. - // add info about window-pane mapping to support this feature. + gui_tabs: RefCell>, + remote_panes: RefCell>, } pub struct TmuxDomain { @@ -62,11 +75,8 @@ impl TmuxDomainState { *self.state.borrow_mut() = State::Idle; let resp = response.clone(); promise::spawn::spawn(async move { - match cmd.process_result(domain_id, &resp) { - Ok(result) => process_command_result(domain_id, result), - Err(err) => { - log::error!("error processing result: {}", err); - } + if let Err(err) = cmd.process_result(domain_id, &resp) { + log::error!("error processing result: {}", err); } }) .detach(); @@ -77,7 +87,7 @@ impl TmuxDomainState { // TODO: write to pane's tmux queue // TODO: decode string } - Event::WindowAdd { window } => { + Event::WindowAdd { window: _ } => { if self.gui_window_id.borrow().is_none() { if let Some(mux) = Mux::get() { let window_builder = mux.new_empty_window(); @@ -138,6 +148,7 @@ impl TmuxDomain { state: RefCell::new(State::WaitForInitialGuard), cmd_queue: RefCell::new(cmd_queue), gui_window_id: RefCell::new(None), + gui_tabs: RefCell::new(Vec::default()), remote_panes: RefCell::new(HashMap::default()), }); Self { inner } diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 42195aa9920..764ac970bb8 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -1,16 +1,12 @@ use crate::domain::DomainId; -use crate::tmux::TmuxDomain; +use crate::tmux::{TmuxDomain, TmuxDomainState}; use crate::Mux; use anyhow::anyhow; use tmux_cc::*; pub(crate) trait TmuxCommand { fn get_command(&self) -> String; - fn process_result( - &self, - domain_id: DomainId, - result: &Guarded, - ) -> anyhow::Result; + fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; } #[derive(Debug)] @@ -27,6 +23,12 @@ pub(crate) struct PaneItem { pane_top: u64, } +impl TmuxDomainState { + fn sync_pane_state(&self, panes: &[PaneItem]) -> anyhow::Result<()> { + Ok(()) + } +} + pub(crate) enum TmuxCommandResult { PaneList(Vec), } @@ -40,11 +42,7 @@ impl TmuxCommand for ListAllPanes { .to_owned() } - fn process_result( - &self, - domain_id: DomainId, - result: &Guarded, - ) -> anyhow::Result { + fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()> { let mut items = vec![]; for line in result.output.split('\n') { @@ -104,16 +102,13 @@ impl TmuxCommand for ListAllPanes { }); } - log::error!("panes in domain_id {}: {:?}", domain_id, items); - Ok(TmuxCommandResult::PaneList(items)) - } -} - -pub(crate) fn process_command_result(domain_id: DomainId, result: TmuxCommandResult) { - let mux = Mux::get().expect("to be called on main thread"); - if let Some(domain) = mux.get_domain(domain_id) { - if let Some(tmux_domain) = domain.downcast_ref::() { - // TODO: + log::info!("panes in domain_id {}: {:?}", domain_id, items); + let mux = Mux::get().expect("to be called on main thread"); + if let Some(domain) = mux.get_domain(domain_id) { + if let Some(tmux_domain) = domain.downcast_ref::() { + return tmux_domain.inner.sync_pane_state(&items); + } } + anyhow::bail!("Tmux domain lost"); } } diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs new file mode 100644 index 00000000000..e165188e21e --- /dev/null +++ b/mux/src/tmux_pty.rs @@ -0,0 +1,74 @@ +use flume; +use portable_pty::MasterPty; +use std::io::{Read, Write}; + +use crate::tmux::RefTmuxRemotePane; + +pub(crate) struct TmuxReader { + rx: flume::Receiver, +} + +impl Read for TmuxReader { + fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { + match self.rx.try_recv() { + Ok(str) => { + return buf.write(str.as_bytes()); + } + Err(_) => { + return Ok(0); + } + } + } +} + +// A local tmux pane(tab) based on a tmux pty +pub(crate) struct TmuxPty { + master_pane: RefTmuxRemotePane, + rx: flume::Receiver, + // TODO: wx +} + +impl Write for TmuxPty { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + // TODO: write to wx of pty + Ok(0) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl MasterPty for TmuxPty { + fn resize(&self, size: portable_pty::PtySize) -> Result<(), anyhow::Error> { + // TODO: perform pane resize + Ok(()) + } + + fn get_size(&self) -> Result { + let pane = self.master_pane.lock().unwrap(); + Ok(portable_pty::PtySize { + rows: pane.pane_height as u16, + cols: pane.pane_width as u16, + pixel_width: 0, + pixel_height: 0, + }) + } + + fn try_clone_reader(&self) -> Result, anyhow::Error> { + Ok(Box::new(TmuxReader { + rx: self.rx.clone(), + })) + } + + fn try_clone_writer(&self) -> Result, anyhow::Error> { + Ok(Box::new(TmuxPty { + master_pane: self.master_pane.clone(), + rx: self.rx.clone(), + })) + } + + fn process_group_leader(&self) -> Option { + return None; + } +} From 62e89e3e66f642d2a3018a8c829e2fef6ede67eb Mon Sep 17 00:00:00 2001 From: g4c Date: Fri, 27 Aug 2021 11:04:41 +0800 Subject: [PATCH 07/21] send tmux output --- mux/src/tmux.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 4749a0b0c65..743b1ef88de 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -84,8 +84,15 @@ impl TmuxDomainState { State::Idle => {} }, Event::Output { pane, text } => { - // TODO: write to pane's tmux queue - // TODO: decode string + let pane_map = self.remote_panes.borrow_mut(); + if let Some(ref_pane) = pane_map.get(pane) { + // TODO: handle escape? + let tmux_pane = ref_pane.lock().unwrap(); + tmux_pane + .tx + .send(text.to_string()) + .expect("send to tmux pane failed"); + } } Event::WindowAdd { window: _ } => { if self.gui_window_id.borrow().is_none() { From 8951278c1381fdbc4d3b5994779545547d908be3 Mon Sep 17 00:00:00 2001 From: g4c Date: Fri, 27 Aug 2021 11:24:13 +0800 Subject: [PATCH 08/21] [WIP] sync tmux panes --- mux/src/tmux.rs | 15 ++++++++++----- mux/src/tmux_commands.rs | 15 +++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 743b1ef88de..07b209cf549 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -45,14 +45,15 @@ pub(crate) struct TmuxTab { } pub(crate) struct TmuxDomainState { - pane_id: PaneId, + pub pane_id: PaneId, pub domain_id: DomainId, // parser: RefCell, state: RefCell, - cmd_queue: RefCell>>, - gui_window_id: RefCell>, - gui_tabs: RefCell>, - remote_panes: RefCell>, + pub cmd_queue: RefCell>>, + pub gui_window_id: RefCell>, + pub gui_tabs: RefCell>, + pub remote_panes: RefCell>, + pub tmux_session: RefCell>, } pub struct TmuxDomain { @@ -106,6 +107,9 @@ impl TmuxDomainState { } } } + Event::SessionChanged { session, name: _ } => { + *self.tmux_session.borrow_mut() = Some(*session); + } _ => {} } } @@ -157,6 +161,7 @@ impl TmuxDomain { gui_window_id: RefCell::new(None), gui_tabs: RefCell::new(Vec::default()), remote_panes: RefCell::new(HashMap::default()), + tmux_session: RefCell::new(None), }); Self { inner } } diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 764ac970bb8..e8b130b94c2 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -25,14 +25,21 @@ pub(crate) struct PaneItem { impl TmuxDomainState { fn sync_pane_state(&self, panes: &[PaneItem]) -> anyhow::Result<()> { + // TODO: + // 1) iter over current session panes + // 2) create pane if not exist + // 3) fetch scroll buffer if new created + // 4) update pane state if exist + let current_session = self.tmux_session.borrow().unwrap_or(0); + for pane in panes.iter() { + if pane.session_id != current_session { + continue; + } + } Ok(()) } } -pub(crate) enum TmuxCommandResult { - PaneList(Vec), -} - pub(crate) struct ListAllPanes; impl TmuxCommand for ListAllPanes { fn get_command(&self) -> String { From ce87b4e0b0f70cccaed4e33e4b79d3b5d4b4997b Mon Sep 17 00:00:00 2001 From: g4c Date: Sun, 29 Aug 2021 16:34:36 +0800 Subject: [PATCH 09/21] it worked! --- mux/src/lib.rs | 2 +- mux/src/tmux.rs | 39 ++++---- mux/src/tmux_commands.rs | 157 ++++++++++++++++++++++++++++++- mux/src/tmux_pty.rs | 27 +++++- termwiz/src/escape/parser/mod.rs | 2 - 5 files changed, 199 insertions(+), 28 deletions(-) diff --git a/mux/src/lib.rs b/mux/src/lib.rs index 6f8f8ca59af..f07b2e6353d 100644 --- a/mux/src/lib.rs +++ b/mux/src/lib.rs @@ -493,7 +493,7 @@ impl Mux { for (window_id, win) in windows.iter_mut() { win.prune_dead_tabs(&live_tab_ids); if win.is_empty() { - log::debug!("prune_dead_windows: window is now empty"); + log::info!("prune_dead_windows: window is now empty"); dead_windows.push(*window_id); } } diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 07b209cf549..2dc74347f93 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -3,12 +3,12 @@ use crate::pane::{Pane, PaneId}; use crate::tab::{SplitDirection, Tab, TabId}; use crate::tmux_commands::{ListAllPanes, TmuxCommand}; use crate::window::WindowId; -use crate::Mux; +use crate::{Mux, MuxWindowBuilder}; use async_trait::async_trait; use flume; use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use tmux_cc::*; @@ -20,14 +20,15 @@ enum State { WaitingForResponse, } +#[derive(Debug)] pub(crate) struct TmuxRemotePane { // members for local - local_pane_id: PaneId, - tx: flume::Sender, + pub local_pane_id: PaneId, + pub tx: flume::Sender, // members sync with remote - session_id: TmuxSessionId, - window_id: TmuxWindowId, - pane_id: TmuxPaneId, + pub session_id: TmuxSessionId, + pub window_id: TmuxWindowId, + pub pane_id: TmuxPaneId, pub cursor_x: u64, pub cursor_y: u64, pub pane_width: u64, @@ -39,9 +40,9 @@ pub(crate) struct TmuxRemotePane { pub(crate) type RefTmuxRemotePane = Arc>; pub(crate) struct TmuxTab { - tab_id: TabId, - tmux_window_id: TmuxWindowId, - panes: Vec, + pub tab_id: TabId, // local tab ID + pub tmux_window_id: TmuxWindowId, + pub panes: HashSet, // tmux panes within tmux window } pub(crate) struct TmuxDomainState { @@ -50,7 +51,7 @@ pub(crate) struct TmuxDomainState { // parser: RefCell, state: RefCell, pub cmd_queue: RefCell>>, - pub gui_window_id: RefCell>, + pub gui_window: RefCell>, pub gui_tabs: RefCell>, pub remote_panes: RefCell>, pub tmux_session: RefCell>, @@ -64,7 +65,7 @@ impl TmuxDomainState { pub fn advance(&self, events: Box>) { for event in events.iter() { let state = *self.state.borrow(); - log::error!("tmux: {:?} in state {:?}", event, state); + log::info!("tmux: {:?} in state {:?}", event, state); match event { Event::Guarded(response) => match state { State::WaitForInitialGuard => { @@ -77,7 +78,7 @@ impl TmuxDomainState { let resp = response.clone(); promise::spawn::spawn(async move { if let Err(err) = cmd.process_result(domain_id, &resp) { - log::error!("error processing result: {}", err); + log::error!("Tmux processing command result error: {}", err); } }) .detach(); @@ -87,28 +88,30 @@ impl TmuxDomainState { Event::Output { pane, text } => { let pane_map = self.remote_panes.borrow_mut(); if let Some(ref_pane) = pane_map.get(pane) { - // TODO: handle escape? let tmux_pane = ref_pane.lock().unwrap(); tmux_pane .tx .send(text.to_string()) .expect("send to tmux pane failed"); + } else { + log::error!("Tmux pane {} havn't been attached", pane); } } Event::WindowAdd { window: _ } => { - if self.gui_window_id.borrow().is_none() { + if self.gui_window.borrow().is_none() { if let Some(mux) = Mux::get() { let window_builder = mux.new_empty_window(); log::info!("Tmux create window id {}", window_builder.window_id); { - let mut window_id = self.gui_window_id.borrow_mut(); - *window_id = Some(window_builder.window_id); + let mut window_id = self.gui_window.borrow_mut(); + *window_id = Some(window_builder); // keep the builder so it won't be purged } } } } Event::SessionChanged { session, name: _ } => { *self.tmux_session.borrow_mut() = Some(*session); + log::info!("tmux session changed:{}", session); } _ => {} } @@ -158,7 +161,7 @@ impl TmuxDomain { // parser, state: RefCell::new(State::WaitForInitialGuard), cmd_queue: RefCell::new(cmd_queue), - gui_window_id: RefCell::new(None), + gui_window: RefCell::new(None), gui_tabs: RefCell::new(Vec::default()), remote_panes: RefCell::new(HashMap::default()), tmux_session: RefCell::new(None), diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index e8b130b94c2..5305eb067a1 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -1,10 +1,19 @@ use crate::domain::DomainId; -use crate::tmux::{TmuxDomain, TmuxDomainState}; +use crate::localpane::LocalPane; +use crate::pane::alloc_pane_id; +use crate::tab::{Tab, TabId}; +use crate::tmux::{TmuxDomain, TmuxDomainState, TmuxRemotePane, TmuxTab}; +use crate::tmux_pty::TmuxPty; use crate::Mux; +use crate::Pane; use anyhow::anyhow; +use portable_pty::{MasterPty, PtySize}; +use std::collections::HashSet; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; use tmux_cc::*; -pub(crate) trait TmuxCommand { +pub(crate) trait TmuxCommand: Send { fn get_command(&self) -> String; fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; } @@ -24,6 +33,54 @@ pub(crate) struct PaneItem { } impl TmuxDomainState { + fn check_pane_attached(&self, target: &PaneItem) -> bool { + let pane_list = self.gui_tabs.borrow(); + let local_tab = match pane_list + .iter() + .find(|&x| x.tmux_window_id == target.window_id) + { + Some(x) => x, + None => { + return false; + } + }; + match local_tab.panes.get(&target.pane_id) { + Some(_) => { + return true; + } + None => { + return false; + } + } + } + + fn add_attached_pane(&self, target: &PaneItem, tab_id: &TabId) -> anyhow::Result<()> { + let mut pane_list = self.gui_tabs.borrow_mut(); + let local_tab = match pane_list + .iter_mut() + .find(|x| x.tmux_window_id == target.window_id) + { + Some(x) => x, + None => { + pane_list.push(TmuxTab { + tab_id: *tab_id, + tmux_window_id: target.window_id, + panes: HashSet::new(), + }); + pane_list.last_mut().unwrap() + } + }; + match local_tab.panes.get(&target.pane_id) { + Some(_) => { + anyhow::bail!("Tmux pane already attached"); + } + None => { + local_tab.panes.insert(target.pane_id); + return Ok(()); + } + } + } + fn sync_pane_state(&self, panes: &[PaneItem]) -> anyhow::Result<()> { // TODO: // 1) iter over current session panes @@ -32,9 +89,76 @@ impl TmuxDomainState { // 4) update pane state if exist let current_session = self.tmux_session.borrow().unwrap_or(0); for pane in panes.iter() { - if pane.session_id != current_session { + if pane.session_id != current_session || self.check_pane_attached(&pane) { continue; } + + let local_pane_id = alloc_pane_id(); + let channel = flume::unbounded::(); + let ref_pane = Arc::new(Mutex::new(TmuxRemotePane { + local_pane_id, + tx: channel.0.clone(), + session_id: pane.session_id, + window_id: pane.window_id, + pane_id: pane.pane_id, + cursor_x: pane.cursor_x, + cursor_y: pane.cursor_y, + pane_width: pane.pane_width, + pane_height: pane.pane_height, + pane_left: pane.pane_left, + pane_top: pane.pane_top, + })); + + { + let mut pane_map = self.remote_panes.borrow_mut(); + pane_map.insert(pane.pane_id, ref_pane.clone()); + } + + let pane_pty = TmuxPty { + rx: channel.1.clone(), + master_pane: ref_pane, + }; + let writer = pane_pty.try_clone_writer().unwrap(); + let mux = Mux::get().expect("should be called at main thread"); + let size = PtySize { + rows: pane.pane_height as u16, + cols: pane.pane_width as u16, + pixel_width: 0, + pixel_height: 0, + }; + + let terminal = wezterm_term::Terminal::new( + crate::pty_size_to_terminal_size(size), + std::sync::Arc::new(config::TermConfig::new()), + "WezTerm", + config::wezterm_version(), + Box::new(writer), + ); + + let local_pane: Rc = Rc::new(LocalPane::new( + local_pane_id, + terminal, + Box::new(pane_pty.clone()), + Box::new(pane_pty.clone()), + self.domain_id, + )); + + let tab = Rc::new(Tab::new(&size)); + tab.assign_pane(&local_pane); + + let mut gui_window = self.gui_window.borrow_mut(); + let gui_window_id = match gui_window.as_mut() { + Some(x) => &mut *x, + None => { + anyhow::bail!("None tmux GUI window created") + } + }; + + mux.add_tab_and_active_pane(&tab)?; + mux.add_tab_to_window(&tab, **gui_window_id)?; + gui_window_id.notify(); + + self.add_attached_pane(&pane, &tab.tab_id())?; } Ok(()) } @@ -119,3 +243,30 @@ impl TmuxCommand for ListAllPanes { anyhow::bail!("Tmux domain lost"); } } + +pub(crate) struct CapturePane(TmuxPaneId); +impl TmuxCommand for CapturePane { + fn get_command(&self) -> String { + format!("capturep -p -t {} -e -C\n", self.0) + } + + fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()> { + let mux = Mux::get().expect("to be called on main thread"); + let domain = match mux.get_domain(domain_id) { + Some(d) => d, + None => anyhow::bail!("Tmux domain lost"), + }; + let tmux_domain = match domain.downcast_ref::() { + Some(t) => t, + None => anyhow::bail!("Tmux domain lost"), + }; + + let pane_map = tmux_domain.inner.remote_panes.borrow(); + if let Some(pane) = pane_map.get(&self.0) { + let lock = pane.lock().unwrap(); + lock.tx.send(result.output.to_owned()).unwrap(); + } + + Ok(()) + } +} diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index e165188e21e..16c540c488b 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -1,5 +1,5 @@ use flume; -use portable_pty::MasterPty; +use portable_pty::{Child, MasterPty}; use std::io::{Read, Write}; use crate::tmux::RefTmuxRemotePane; @@ -10,7 +10,7 @@ pub(crate) struct TmuxReader { impl Read for TmuxReader { fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { - match self.rx.try_recv() { + match self.rx.recv() { Ok(str) => { return buf.write(str.as_bytes()); } @@ -22,9 +22,10 @@ impl Read for TmuxReader { } // A local tmux pane(tab) based on a tmux pty +#[derive(Debug, Clone)] pub(crate) struct TmuxPty { - master_pane: RefTmuxRemotePane, - rx: flume::Receiver, + pub master_pane: RefTmuxRemotePane, + pub rx: flume::Receiver, // TODO: wx } @@ -39,6 +40,24 @@ impl Write for TmuxPty { } } +impl Child for TmuxPty { + fn try_wait(&mut self) -> std::io::Result> { + todo!() + } + + fn kill(&mut self) -> std::io::Result<()> { + todo!() + } + + fn wait(&mut self) -> std::io::Result { + loop {} + } + + fn process_id(&self) -> Option { + Some(0) + } +} + impl MasterPty for TmuxPty { fn resize(&self, size: portable_pty::PtySize) -> Result<(), anyhow::Error> { // TODO: perform pane resize diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 767536a8db3..b79dee43e11 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -91,7 +91,6 @@ impl Parser { let mut tmux_parser = tmux_state.borrow_mut(); // TODO: wrap events into some Result to capture bytes cannot be parsed let tmux_events = tmux_parser.advance_bytes(bytes); - log::info!("parsed tmux events: {:?}", &tmux_events); callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( Box::new(tmux_events), ))); @@ -257,7 +256,6 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> { if let Some(tmux_state) = &self.state.tmux_state { let mut tmux_parser = tmux_state.borrow_mut(); if let Some(tmux_event) = tmux_parser.advance_byte(data) { - log::info!("parsed tmux event:{:?}", &tmux_event); (self.callback)(Action::DeviceControl(DeviceControlMode::TmuxEvents( Box::new(vec![tmux_event]), ))); From 493e74adb77de6687ba037791a6d05fa0e0c194d Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 30 Aug 2021 16:12:59 +0800 Subject: [PATCH 10/21] create gui when attach to exist session --- mux/src/tmux.rs | 23 +++++++++++++---------- mux/src/tmux_commands.rs | 6 ++++-- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 2dc74347f93..cc32c9e59cc 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -98,16 +98,7 @@ impl TmuxDomainState { } } Event::WindowAdd { window: _ } => { - if self.gui_window.borrow().is_none() { - if let Some(mux) = Mux::get() { - let window_builder = mux.new_empty_window(); - log::info!("Tmux create window id {}", window_builder.window_id); - { - let mut window_id = self.gui_window.borrow_mut(); - *window_id = Some(window_builder); // keep the builder so it won't be purged - } - } - } + self.create_gui_window(); } Event::SessionChanged { session, name: _ } => { *self.tmux_session.borrow_mut() = Some(*session); @@ -147,6 +138,18 @@ impl TmuxDomainState { *self.state.borrow_mut() = State::WaitingForResponse; } } + + pub fn create_gui_window(&self) { + if self.gui_window.borrow().is_none() { + let mux = Mux::get().expect("should be call at main thread"); + let window_builder = mux.new_empty_window(); + log::info!("Tmux create window id {}", window_builder.window_id); + { + let mut window_id = self.gui_window.borrow_mut(); + *window_id = Some(window_builder); // keep the builder so it won't be purged + } + }; + } } impl TmuxDomain { diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 5305eb067a1..ae184d4512e 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -146,11 +146,12 @@ impl TmuxDomainState { let tab = Rc::new(Tab::new(&size)); tab.assign_pane(&local_pane); + self.create_gui_window(); let mut gui_window = self.gui_window.borrow_mut(); let gui_window_id = match gui_window.as_mut() { - Some(x) => &mut *x, + Some(x) => x, None => { - anyhow::bail!("None tmux GUI window created") + anyhow::bail!("No tmux gui created"); } }; @@ -159,6 +160,7 @@ impl TmuxDomainState { gui_window_id.notify(); self.add_attached_pane(&pane, &tab.tab_id())?; + log::info!("new pane attached"); } Ok(()) } From 95449a866b207a049270e53ee7410b768263f074 Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 1 Sep 2021 11:29:37 +0800 Subject: [PATCH 11/21] condvar for session release --- mux/src/tmux.rs | 3 ++- mux/src/tmux_commands.rs | 6 +++++- mux/src/tmux_pty.rs | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index cc32c9e59cc..d4dc2f98bd7 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -10,7 +10,7 @@ use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; use tmux_cc::*; #[derive(PartialEq, Eq, Debug, Copy, Clone)] @@ -25,6 +25,7 @@ pub(crate) struct TmuxRemotePane { // members for local pub local_pane_id: PaneId, pub tx: flume::Sender, + pub active_lock: Arc<(Mutex, Condvar)>, // members sync with remote pub session_id: TmuxSessionId, pub window_id: TmuxWindowId, diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index ae184d4512e..0ef33a74a12 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -10,7 +10,7 @@ use anyhow::anyhow; use portable_pty::{MasterPty, PtySize}; use std::collections::HashSet; use std::rc::Rc; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; use tmux_cc::*; pub(crate) trait TmuxCommand: Send { @@ -95,9 +95,12 @@ impl TmuxDomainState { let local_pane_id = alloc_pane_id(); let channel = flume::unbounded::(); + let active_lock = Arc::new((Mutex::new(false), Condvar::new())); + let ref_pane = Arc::new(Mutex::new(TmuxRemotePane { local_pane_id, tx: channel.0.clone(), + active_lock: active_lock.clone(), session_id: pane.session_id, window_id: pane.window_id, pane_id: pane.pane_id, @@ -116,6 +119,7 @@ impl TmuxDomainState { let pane_pty = TmuxPty { rx: channel.1.clone(), + active_lock: active_lock.clone(), master_pane: ref_pane, }; let writer = pane_pty.try_clone_writer().unwrap(); diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index 16c540c488b..bec2d88d079 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -1,6 +1,9 @@ use flume; -use portable_pty::{Child, MasterPty}; -use std::io::{Read, Write}; +use portable_pty::{Child, ExitStatus, MasterPty}; +use std::{ + io::{Read, Write}, + sync::{Arc, Condvar, Mutex}, +}; use crate::tmux::RefTmuxRemotePane; @@ -26,6 +29,7 @@ impl Read for TmuxReader { pub(crate) struct TmuxPty { pub master_pane: RefTmuxRemotePane, pub rx: flume::Receiver, + pub active_lock: Arc<(Mutex, Condvar)>, // TODO: wx } @@ -50,7 +54,12 @@ impl Child for TmuxPty { } fn wait(&mut self) -> std::io::Result { - loop {} + let (lock, var) = &*self.active_lock; + let mut released = lock.lock().unwrap(); + while !*released { + released = var.wait(released).unwrap(); + } + return Ok(ExitStatus::with_exit_code(0)); } fn process_id(&self) -> Option { @@ -84,6 +93,7 @@ impl MasterPty for TmuxPty { Ok(Box::new(TmuxPty { master_pane: self.master_pane.clone(), rx: self.rx.clone(), + active_lock: self.active_lock.clone(), })) } From 07239480cee18ece5d658469dd08d5c8456bac12 Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 1 Sep 2021 17:59:38 +0800 Subject: [PATCH 12/21] nicely quit tmux mode --- mux/src/tmux.rs | 10 +++++++ termwiz/src/escape/parser/mod.rs | 48 ++++++++++++++++++++++++-------- tmux-cc/src/lib.rs | 40 ++++++++++++++++---------- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index d4dc2f98bd7..a1f579ab9d4 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -105,6 +105,16 @@ impl TmuxDomainState { *self.tmux_session.borrow_mut() = Some(*session); log::info!("tmux session changed:{}", session); } + Event::Exit { reason: _ } => { + let mut pane_map = self.remote_panes.borrow_mut(); + for (_, v) in pane_map.iter_mut() { + let remote_pane = v.lock().unwrap(); + let (lock, condvar) = &*remote_pane.active_lock; + let mut released = lock.lock().unwrap(); + *released = true; + condvar.notify_all(); + } + } _ => {} } } diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index b79dee43e11..9ede703b59f 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -86,14 +86,30 @@ impl Parser { pub fn parse(&mut self, bytes: &[u8], mut callback: F) { let tmux_state: bool = self.state.borrow().tmux_state.is_some(); if tmux_state { - let parser_state = self.state.borrow(); - let tmux_state = parser_state.tmux_state.as_ref().unwrap(); - let mut tmux_parser = tmux_state.borrow_mut(); - // TODO: wrap events into some Result to capture bytes cannot be parsed - let tmux_events = tmux_parser.advance_bytes(bytes); - callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( - Box::new(tmux_events), - ))); + if let Some(unparsed_str) = { + let parser_state = self.state.borrow(); + let tmux_state = parser_state.tmux_state.as_ref().unwrap(); + let mut tmux_parser = tmux_state.borrow_mut(); + // TODO: wrap events into some Result to capture bytes cannot be parsed + match tmux_parser.advance_bytes(bytes) { + Ok(tmux_events) => { + callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(tmux_events), + ))); + None + } + Err(err_buf) => Some(err_buf.to_string().to_owned()), + } + } { + let mut parser_state = self.state.borrow_mut(); + parser_state.tmux_state = None; + let mut perform = Performer { + callback: &mut callback, + state: &mut parser_state, + }; + self.state_machine + .parse(unparsed_str.as_bytes(), &mut perform); + } } else { let mut perform = Performer { callback: &mut callback, @@ -255,10 +271,18 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> { } else { if let Some(tmux_state) = &self.state.tmux_state { let mut tmux_parser = tmux_state.borrow_mut(); - if let Some(tmux_event) = tmux_parser.advance_byte(data) { - (self.callback)(Action::DeviceControl(DeviceControlMode::TmuxEvents( - Box::new(vec![tmux_event]), - ))); + match tmux_parser.advance_byte(data) { + Ok(optional_events) => { + if let Some(tmux_event) = optional_events { + (self.callback)(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(vec![tmux_event]), + ))); + } + } + Err(err) => { + drop(tmux_parser); + self.state.tmux_state = None; // drop tmux state + } } } else { (self.callback)(Action::DeviceControl(DeviceControlMode::Data(data))); diff --git a/tmux-cc/src/lib.rs b/tmux-cc/src/lib.rs index e58f3040741..9659cf75982 100644 --- a/tmux-cc/src/lib.rs +++ b/tmux-cc/src/lib.rs @@ -489,30 +489,42 @@ impl Parser { } } - pub fn advance_byte(&mut self, c: u8) -> Option { + pub fn advance_byte(&mut self, c: u8) -> anyhow::Result> { if c == b'\n' { self.process_line() } else { self.buffer.push(c); - None + Ok(None) } } - pub fn advance_string(&mut self, s: &str) -> Vec { + pub fn advance_string(&mut self, s: &str) -> anyhow::Result> { self.advance_bytes(s.as_bytes()) } - pub fn advance_bytes(&mut self, bytes: &[u8]) -> Vec { + pub fn advance_bytes(&mut self, bytes: &[u8]) -> anyhow::Result> { let mut events = vec![]; - for &b in bytes { - if let Some(event) = self.advance_byte(b) { - events.push(event); + for (i, &b) in bytes.iter().enumerate() { + match self.advance_byte(b) { + Ok(option_event) => { + if let Some(e) = option_event { + events.push(e); + } + } + Err(err) => { + // TODO: concat remained bytes + return Err(anyhow::anyhow!(format!( + "{}{}", + err, + std::str::from_utf8(bytes).unwrap() + ))); + } } } - events + Ok(events) } - fn process_guarded_line(&mut self, line: String) -> Option { + fn process_guarded_line(&mut self, line: String) -> anyhow::Result> { let result = match parse_line(&line) { Ok(Event::End { timestamp, @@ -563,10 +575,10 @@ impl Parser { } }; self.buffer.clear(); - return result; + return Ok(result); } - fn process_line(&mut self) -> Option { + fn process_line(&mut self) -> anyhow::Result> { if self.buffer.last() == Some(&b'\r') { self.buffer.pop(); } @@ -597,7 +609,7 @@ impl Parser { Ok(event) => Some(event), Err(err) => { log::error!("Unrecognized tmux cc line: {}", err); - None + return Err(anyhow::anyhow!(line.to_owned())); } } } @@ -607,7 +619,7 @@ impl Parser { } }; self.buffer.clear(); - result + Ok(result) } } @@ -667,7 +679,7 @@ here "; let mut p = Parser::new(); - let events = p.advance_bytes(input); + let events = p.advance_bytes(input).unwrap(); assert_eq!( vec![ Event::SessionsChanged, From 4feedd3199e6652c8b3928f23038b455440f4efd Mon Sep 17 00:00:00 2001 From: g4c Date: Sun, 5 Sep 2021 14:58:38 +0800 Subject: [PATCH 13/21] concat tmux parser buffer and unparsed bytes --- termwiz/src/escape/parser/mod.rs | 2 +- tmux-cc/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 9ede703b59f..9e108dd7e40 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -279,7 +279,7 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> { ))); } } - Err(err) => { + Err(_) => { drop(tmux_parser); self.state.tmux_state = None; // drop tmux state } diff --git a/tmux-cc/src/lib.rs b/tmux-cc/src/lib.rs index 9659cf75982..71cee1de94d 100644 --- a/tmux-cc/src/lib.rs +++ b/tmux-cc/src/lib.rs @@ -516,7 +516,7 @@ impl Parser { return Err(anyhow::anyhow!(format!( "{}{}", err, - std::str::from_utf8(bytes).unwrap() + std::str::from_utf8(&bytes[i..]).unwrap() ))); } } From 5fb247599380b3080e423b43198ee53abd067cab Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 15 Sep 2021 11:14:39 +0800 Subject: [PATCH 14/21] write into tmux --- mux/src/tmux.rs | 38 +++++++++++++++++++++++--------------- mux/src/tmux_commands.rs | 28 +++++++++++++++++++++++++++- mux/src/tmux_pty.rs | 23 +++++++++++++++++++---- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index a1f579ab9d4..779daa78390 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -46,12 +46,13 @@ pub(crate) struct TmuxTab { pub panes: HashSet, // tmux panes within tmux window } +pub(crate) type TmuxCmdQueue = VecDeque>; pub(crate) struct TmuxDomainState { pub pane_id: PaneId, pub domain_id: DomainId, // parser: RefCell, state: RefCell, - pub cmd_queue: RefCell>>, + pub cmd_queue: Arc>, pub gui_window: RefCell>, pub gui_tabs: RefCell>, pub remote_panes: RefCell>, @@ -73,7 +74,8 @@ impl TmuxDomainState { *self.state.borrow_mut() = State::Idle; } State::WaitingForResponse => { - let cmd = self.cmd_queue.borrow_mut().pop_front().unwrap(); + let mut cmd_queue = self.cmd_queue.as_ref().lock().unwrap(); + let cmd = cmd_queue.pop_front().unwrap(); let domain_id = self.domain_id; *self.state.borrow_mut() = State::Idle; let resp = response.clone(); @@ -120,17 +122,9 @@ impl TmuxDomainState { } // send pending commands to tmux - if *self.state.borrow() == State::Idle && !self.cmd_queue.borrow().is_empty() { - let domain_id = self.domain_id; - promise::spawn::spawn(async move { - let mux = Mux::get().expect("to be called on main thread"); - if let Some(domain) = mux.get_domain(domain_id) { - if let Some(tmux_domain) = domain.downcast_ref::() { - tmux_domain.send_next_command(); - } - } - }) - .detach(); + let cmd_queue = self.cmd_queue.as_ref().lock().unwrap(); + if *self.state.borrow() == State::Idle && !cmd_queue.is_empty() { + TmuxDomainState::kick_off(self.domain_id); } } @@ -138,7 +132,8 @@ impl TmuxDomainState { if *self.state.borrow() != State::Idle { return; } - if let Some(first) = self.cmd_queue.borrow().front() { + let cmd_queue = self.cmd_queue.as_ref().lock().unwrap(); + if let Some(first) = cmd_queue.front() { let cmd = first.get_command(); log::error!("sending cmd {:?}", cmd); let mux = Mux::get().expect("to be called on main thread"); @@ -150,6 +145,18 @@ impl TmuxDomainState { } } + pub fn kick_off(domain_id: usize) { + promise::spawn::spawn_into_main_thread(async move { + let mux = Mux::get().expect("to be called on main thread"); + if let Some(domain) = mux.get_domain(domain_id) { + if let Some(tmux_domain) = domain.downcast_ref::() { + tmux_domain.send_next_command(); + } + } + }) + .detach(); + } + pub fn create_gui_window(&self) { if self.gui_window.borrow().is_none() { let mux = Mux::get().expect("should be call at main thread"); @@ -174,12 +181,13 @@ impl TmuxDomain { pane_id, // parser, state: RefCell::new(State::WaitForInitialGuard), - cmd_queue: RefCell::new(cmd_queue), + cmd_queue: Arc::new(Mutex::new(cmd_queue)), gui_window: RefCell::new(None), gui_tabs: RefCell::new(Vec::default()), remote_panes: RefCell::new(HashMap::default()), tmux_session: RefCell::new(None), }); + Self { inner } } diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 0ef33a74a12..ed4464ef23d 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -9,11 +9,13 @@ use crate::Pane; use anyhow::anyhow; use portable_pty::{MasterPty, PtySize}; use std::collections::HashSet; +use std::fmt::Debug; +use std::fmt::Write; use std::rc::Rc; use std::sync::{Arc, Condvar, Mutex}; use tmux_cc::*; -pub(crate) trait TmuxCommand: Send { +pub(crate) trait TmuxCommand: Send + Debug { fn get_command(&self) -> String; fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; } @@ -118,7 +120,9 @@ impl TmuxDomainState { } let pane_pty = TmuxPty { + domain_id: self.domain_id, rx: channel.1.clone(), + cmd_queue: self.cmd_queue.clone(), active_lock: active_lock.clone(), master_pane: ref_pane, }; @@ -170,6 +174,7 @@ impl TmuxDomainState { } } +#[derive(Debug)] pub(crate) struct ListAllPanes; impl TmuxCommand for ListAllPanes { fn get_command(&self) -> String { @@ -250,6 +255,7 @@ impl TmuxCommand for ListAllPanes { } } +#[derive(Debug)] pub(crate) struct CapturePane(TmuxPaneId); impl TmuxCommand for CapturePane { fn get_command(&self) -> String { @@ -276,3 +282,23 @@ impl TmuxCommand for CapturePane { Ok(()) } } + +#[derive(Debug)] +pub(crate) struct SendKeys { + pub keys: Vec, + pub pane: TmuxPaneId, +} +impl TmuxCommand for SendKeys { + fn get_command(&self) -> String { + let mut s = String::new(); + for &byte in self.keys.iter() { + write!(&mut s, "0x{:X}\r", byte).expect("unable to write key"); + } + format!("send-keys -t {} {}", self.pane, s) + // FIXME: An unexpected duplicated command will prompt next line, why? + } + + fn process_result(&self, _domain_id: DomainId, _result: &Guarded) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index bec2d88d079..d9ee78048f6 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -1,3 +1,7 @@ +use crate::{ + tmux::{RefTmuxRemotePane, TmuxCmdQueue, TmuxDomainState}, + tmux_commands::SendKeys, +}; use flume; use portable_pty::{Child, ExitStatus, MasterPty}; use std::{ @@ -5,8 +9,6 @@ use std::{ sync::{Arc, Condvar, Mutex}, }; -use crate::tmux::RefTmuxRemotePane; - pub(crate) struct TmuxReader { rx: flume::Receiver, } @@ -27,15 +29,26 @@ impl Read for TmuxReader { // A local tmux pane(tab) based on a tmux pty #[derive(Debug, Clone)] pub(crate) struct TmuxPty { + pub domain_id: usize, pub master_pane: RefTmuxRemotePane, pub rx: flume::Receiver, + pub cmd_queue: Arc>, pub active_lock: Arc<(Mutex, Condvar)>, - // TODO: wx } impl Write for TmuxPty { fn write(&mut self, buf: &[u8]) -> std::io::Result { - // TODO: write to wx of pty + let pane_id = { + let pane_lock = self.master_pane.lock().unwrap(); + pane_lock.pane_id + }; + log::info!("pane:{}, content:{:?}", &pane_id, buf); + let mut cmd_queue = self.cmd_queue.lock().unwrap(); + cmd_queue.push_back(Box::new(SendKeys { + pane: pane_id, + keys: buf.to_vec(), + })); + TmuxDomainState::kick_off(self.domain_id); Ok(0) } @@ -91,8 +104,10 @@ impl MasterPty for TmuxPty { fn try_clone_writer(&self) -> Result, anyhow::Error> { Ok(Box::new(TmuxPty { + domain_id: self.domain_id, master_pane: self.master_pane.clone(), rx: self.rx.clone(), + cmd_queue: self.cmd_queue.clone(), active_lock: self.active_lock.clone(), })) } From 62ba6409419bd15998cf739367b21f749c6158df Mon Sep 17 00:00:00 2001 From: dixeran Date: Sun, 19 Sep 2021 11:46:37 +0800 Subject: [PATCH 15/21] fix windows build --- mux/src/tmux_pty.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index d9ee78048f6..78587eb8d61 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -78,6 +78,11 @@ impl Child for TmuxPty { fn process_id(&self) -> Option { Some(0) } + + #[cfg(windows)] + fn as_raw_handle(&self) -> Option { + None + } } impl MasterPty for TmuxPty { @@ -112,6 +117,7 @@ impl MasterPty for TmuxPty { })) } + #[cfg(unix)] fn process_group_leader(&self) -> Option { return None; } From 35ca9339b04639e95a849423a942aad65719d73d Mon Sep 17 00:00:00 2001 From: g4c Date: Mon, 20 Sep 2021 13:55:59 +0800 Subject: [PATCH 16/21] fix send-keys tmux command --- mux/src/tmux_commands.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index ed4464ef23d..e4a9d7d1c98 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -292,10 +292,10 @@ impl TmuxCommand for SendKeys { fn get_command(&self) -> String { let mut s = String::new(); for &byte in self.keys.iter() { - write!(&mut s, "0x{:X}\r", byte).expect("unable to write key"); + write!(&mut s, "0x{:X} ", byte).expect("unable to write key"); } - format!("send-keys -t {} {}", self.pane, s) - // FIXME: An unexpected duplicated command will prompt next line, why? + format!("send-keys -t {} {}\r", self.pane, s) + // FIXME: disable ESC k sequence } fn process_result(&self, _domain_id: DomainId, _result: &Guarded) -> anyhow::Result<()> { From 1fb95dbf8a10a3d56926bb57594586d864e037ab Mon Sep 17 00:00:00 2001 From: g4c Date: Thu, 23 Sep 2021 11:45:51 +0800 Subject: [PATCH 17/21] doc and cleanup --- mux/src/tmux.rs | 16 ++++++++++------ mux/src/tmux_commands.rs | 4 +++- mux/src/tmux_pty.rs | 7 ++++--- termwiz/src/escape/parser/mod.rs | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 779daa78390..40e8c1bb014 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -5,7 +5,6 @@ use crate::tmux_commands::{ListAllPanes, TmuxCommand}; use crate::window::WindowId; use crate::{Mux, MuxWindowBuilder}; use async_trait::async_trait; -use flume; use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; @@ -40,6 +39,8 @@ pub(crate) struct TmuxRemotePane { pub(crate) type RefTmuxRemotePane = Arc>; +/// As a remote TmuxTab, keeping the TmuxPanes ID +/// within the remote tab. pub(crate) struct TmuxTab { pub tab_id: TabId, // local tab ID pub tmux_window_id: TmuxWindowId, @@ -48,9 +49,8 @@ pub(crate) struct TmuxTab { pub(crate) type TmuxCmdQueue = VecDeque>; pub(crate) struct TmuxDomainState { - pub pane_id: PaneId, - pub domain_id: DomainId, - // parser: RefCell, + pub pane_id: PaneId, // ID of the original pane + pub domain_id: DomainId, // ID of TmuxDomain state: RefCell, pub cmd_queue: Arc>, pub gui_window: RefCell>, @@ -124,10 +124,12 @@ impl TmuxDomainState { // send pending commands to tmux let cmd_queue = self.cmd_queue.as_ref().lock().unwrap(); if *self.state.borrow() == State::Idle && !cmd_queue.is_empty() { - TmuxDomainState::kick_off(self.domain_id); + TmuxDomainState::schedule_send_next_command(self.domain_id); } } + /// send next command at the front of cmd_queue. + /// must be called inside main thread fn send_next_command(&self) { if *self.state.borrow() != State::Idle { return; @@ -145,7 +147,8 @@ impl TmuxDomainState { } } - pub fn kick_off(domain_id: usize) { + /// schedule a `send_next_command` into main thread + pub fn schedule_send_next_command(domain_id: usize) { promise::spawn::spawn_into_main_thread(async move { let mux = Mux::get().expect("to be called on main thread"); if let Some(domain) = mux.get_domain(domain_id) { @@ -157,6 +160,7 @@ impl TmuxDomainState { .detach(); } + /// create a standalone window for tmux tabs pub fn create_gui_window(&self) { if self.gui_window.borrow().is_none() { let mux = Mux::get().expect("should be call at main thread"); diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index e4a9d7d1c98..5a036b588a4 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -35,6 +35,7 @@ pub(crate) struct PaneItem { } impl TmuxDomainState { + /// check if a PaneItem received from ListAllPanes has been attached fn check_pane_attached(&self, target: &PaneItem) -> bool { let pane_list = self.gui_tabs.borrow(); let local_tab = match pane_list @@ -56,6 +57,8 @@ impl TmuxDomainState { } } + /// after we create a tab for a remote pane, save its ID into the + /// TmuxPane-TmuxPane tree, so we can ref it later. fn add_attached_pane(&self, target: &PaneItem, tab_id: &TabId) -> anyhow::Result<()> { let mut pane_list = self.gui_tabs.borrow_mut(); let local_tab = match pane_list @@ -295,7 +298,6 @@ impl TmuxCommand for SendKeys { write!(&mut s, "0x{:X} ", byte).expect("unable to write key"); } format!("send-keys -t {} {}\r", self.pane, s) - // FIXME: disable ESC k sequence } fn process_result(&self, _domain_id: DomainId, _result: &Guarded) -> anyhow::Result<()> { diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index 78587eb8d61..da3f199199b 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -2,7 +2,6 @@ use crate::{ tmux::{RefTmuxRemotePane, TmuxCmdQueue, TmuxDomainState}, tmux_commands::SendKeys, }; -use flume; use portable_pty::{Child, ExitStatus, MasterPty}; use std::{ io::{Read, Write}, @@ -26,13 +25,15 @@ impl Read for TmuxReader { } } -// A local tmux pane(tab) based on a tmux pty +/// A local tmux pane(tab) based on a tmux pty #[derive(Debug, Clone)] pub(crate) struct TmuxPty { pub domain_id: usize, pub master_pane: RefTmuxRemotePane, pub rx: flume::Receiver, pub cmd_queue: Arc>, + + /// would be released by TmuxDomain when detatched pub active_lock: Arc<(Mutex, Condvar)>, } @@ -48,7 +49,7 @@ impl Write for TmuxPty { pane: pane_id, keys: buf.to_vec(), })); - TmuxDomainState::kick_off(self.domain_id); + TmuxDomainState::schedule_send_next_command(self.domain_id); Ok(0) } diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 9e108dd7e40..20df63478b7 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -84,8 +84,8 @@ impl Parser { } pub fn parse(&mut self, bytes: &[u8], mut callback: F) { - let tmux_state: bool = self.state.borrow().tmux_state.is_some(); - if tmux_state { + let is_tmux_mode: bool = self.state.borrow().tmux_state.is_some(); + if is_tmux_mode { if let Some(unparsed_str) = { let parser_state = self.state.borrow(); let tmux_state = parser_state.tmux_state.as_ref().unwrap(); From 7cf8d2b5d4851e8b5cdb0c7f6e20bb6a10c7b147 Mon Sep 17 00:00:00 2001 From: g4c Date: Thu, 23 Sep 2021 19:50:17 +0800 Subject: [PATCH 18/21] fix log level --- mux/src/tmux_pty.rs | 2 +- termwiz/src/escape/parser/mod.rs | 49 +++++++++++++++++--------------- tmux-cc/src/lib.rs | 4 +-- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index da3f199199b..0a016e71c38 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -43,7 +43,7 @@ impl Write for TmuxPty { let pane_lock = self.master_pane.lock().unwrap(); pane_lock.pane_id }; - log::info!("pane:{}, content:{:?}", &pane_id, buf); + log::trace!("pane:{}, content:{:?}", &pane_id, buf); let mut cmd_queue = self.cmd_queue.lock().unwrap(); cmd_queue.push_back(Box::new(SendKeys { pane: pane_id, diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 20df63478b7..767e5c1e2ee 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -9,7 +9,7 @@ use num_traits::FromPrimitive; use regex::bytes::Regex; use std::borrow::{Borrow, BorrowMut}; use std::cell::{Ref, RefCell}; -use tmux_cc; +use tmux_cc::Event; use vtparse::{CsiParam, VTActor, VTParser}; struct SixelBuilder { @@ -83,32 +83,35 @@ impl Parser { } } + /// advance with tmux parser, bypass VTParse + fn advance_tmux_bytes(&mut self, bytes: &[u8]) -> anyhow::Result> { + let parser_state = self.state.borrow(); + let tmux_state = parser_state.tmux_state.as_ref().unwrap(); + let mut tmux_parser = tmux_state.borrow_mut(); + return tmux_parser.advance_bytes(bytes); + } + pub fn parse(&mut self, bytes: &[u8], mut callback: F) { let is_tmux_mode: bool = self.state.borrow().tmux_state.is_some(); if is_tmux_mode { - if let Some(unparsed_str) = { - let parser_state = self.state.borrow(); - let tmux_state = parser_state.tmux_state.as_ref().unwrap(); - let mut tmux_parser = tmux_state.borrow_mut(); - // TODO: wrap events into some Result to capture bytes cannot be parsed - match tmux_parser.advance_bytes(bytes) { - Ok(tmux_events) => { - callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( - Box::new(tmux_events), - ))); - None - } - Err(err_buf) => Some(err_buf.to_string().to_owned()), + match self.advance_tmux_bytes(bytes) { + Ok(tmux_events) => { + callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(tmux_events), + ))); + } + Err(err_buf) => { + // capture bytes cannot be parsed + let unparsed_str = err_buf.to_string().to_owned(); + let mut parser_state = self.state.borrow_mut(); + parser_state.tmux_state = None; + let mut perform = Performer { + callback: &mut callback, + state: &mut parser_state, + }; + self.state_machine + .parse(unparsed_str.as_bytes(), &mut perform); } - } { - let mut parser_state = self.state.borrow_mut(); - parser_state.tmux_state = None; - let mut perform = Performer { - callback: &mut callback, - state: &mut parser_state, - }; - self.state_machine - .parse(unparsed_str.as_bytes(), &mut perform); } } else { let mut perform = Performer { diff --git a/tmux-cc/src/lib.rs b/tmux-cc/src/lib.rs index 71cee1de94d..631da97e43a 100644 --- a/tmux-cc/src/lib.rs +++ b/tmux-cc/src/lib.rs @@ -512,11 +512,11 @@ impl Parser { } } Err(err) => { - // TODO: concat remained bytes + // concat remained bytes after digested bytes return Err(anyhow::anyhow!(format!( "{}{}", err, - std::str::from_utf8(&bytes[i..]).unwrap() + String::from_utf8_lossy(&bytes[i..]) ))); } } From 179844054dddcaec53bbc941a1b49eb346e2f0c7 Mon Sep 17 00:00:00 2001 From: g4c Date: Thu, 23 Sep 2021 19:50:17 +0800 Subject: [PATCH 19/21] fix log level --- mux/src/tmux.rs | 2 +- mux/src/tmux_pty.rs | 2 +- termwiz/src/escape/parser/mod.rs | 49 +++++++++++++++++--------------- tmux-cc/src/lib.rs | 4 +-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 40e8c1bb014..5f941e32c21 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -137,7 +137,7 @@ impl TmuxDomainState { let cmd_queue = self.cmd_queue.as_ref().lock().unwrap(); if let Some(first) = cmd_queue.front() { let cmd = first.get_command(); - log::error!("sending cmd {:?}", cmd); + log::info!("sending cmd {:?}", cmd); let mux = Mux::get().expect("to be called on main thread"); if let Some(pane) = mux.get_pane(self.pane_id) { let mut writer = pane.writer(); diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index da3f199199b..0a016e71c38 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -43,7 +43,7 @@ impl Write for TmuxPty { let pane_lock = self.master_pane.lock().unwrap(); pane_lock.pane_id }; - log::info!("pane:{}, content:{:?}", &pane_id, buf); + log::trace!("pane:{}, content:{:?}", &pane_id, buf); let mut cmd_queue = self.cmd_queue.lock().unwrap(); cmd_queue.push_back(Box::new(SendKeys { pane: pane_id, diff --git a/termwiz/src/escape/parser/mod.rs b/termwiz/src/escape/parser/mod.rs index 20df63478b7..767e5c1e2ee 100644 --- a/termwiz/src/escape/parser/mod.rs +++ b/termwiz/src/escape/parser/mod.rs @@ -9,7 +9,7 @@ use num_traits::FromPrimitive; use regex::bytes::Regex; use std::borrow::{Borrow, BorrowMut}; use std::cell::{Ref, RefCell}; -use tmux_cc; +use tmux_cc::Event; use vtparse::{CsiParam, VTActor, VTParser}; struct SixelBuilder { @@ -83,32 +83,35 @@ impl Parser { } } + /// advance with tmux parser, bypass VTParse + fn advance_tmux_bytes(&mut self, bytes: &[u8]) -> anyhow::Result> { + let parser_state = self.state.borrow(); + let tmux_state = parser_state.tmux_state.as_ref().unwrap(); + let mut tmux_parser = tmux_state.borrow_mut(); + return tmux_parser.advance_bytes(bytes); + } + pub fn parse(&mut self, bytes: &[u8], mut callback: F) { let is_tmux_mode: bool = self.state.borrow().tmux_state.is_some(); if is_tmux_mode { - if let Some(unparsed_str) = { - let parser_state = self.state.borrow(); - let tmux_state = parser_state.tmux_state.as_ref().unwrap(); - let mut tmux_parser = tmux_state.borrow_mut(); - // TODO: wrap events into some Result to capture bytes cannot be parsed - match tmux_parser.advance_bytes(bytes) { - Ok(tmux_events) => { - callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( - Box::new(tmux_events), - ))); - None - } - Err(err_buf) => Some(err_buf.to_string().to_owned()), + match self.advance_tmux_bytes(bytes) { + Ok(tmux_events) => { + callback(Action::DeviceControl(DeviceControlMode::TmuxEvents( + Box::new(tmux_events), + ))); + } + Err(err_buf) => { + // capture bytes cannot be parsed + let unparsed_str = err_buf.to_string().to_owned(); + let mut parser_state = self.state.borrow_mut(); + parser_state.tmux_state = None; + let mut perform = Performer { + callback: &mut callback, + state: &mut parser_state, + }; + self.state_machine + .parse(unparsed_str.as_bytes(), &mut perform); } - } { - let mut parser_state = self.state.borrow_mut(); - parser_state.tmux_state = None; - let mut perform = Performer { - callback: &mut callback, - state: &mut parser_state, - }; - self.state_machine - .parse(unparsed_str.as_bytes(), &mut perform); } } else { let mut perform = Performer { diff --git a/tmux-cc/src/lib.rs b/tmux-cc/src/lib.rs index 71cee1de94d..631da97e43a 100644 --- a/tmux-cc/src/lib.rs +++ b/tmux-cc/src/lib.rs @@ -512,11 +512,11 @@ impl Parser { } } Err(err) => { - // TODO: concat remained bytes + // concat remained bytes after digested bytes return Err(anyhow::anyhow!(format!( "{}{}", err, - std::str::from_utf8(&bytes[i..]).unwrap() + String::from_utf8_lossy(&bytes[i..]) ))); } } From 3e219c3d25a2fe32012728ecd73edf7699bf1b97 Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 29 Sep 2021 16:50:33 +0800 Subject: [PATCH 20/21] clean unwrap --- mux/src/tmux_commands.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mux/src/tmux_commands.rs b/mux/src/tmux_commands.rs index 5a036b588a4..3c9260e5f59 100644 --- a/mux/src/tmux_commands.rs +++ b/mux/src/tmux_commands.rs @@ -129,7 +129,7 @@ impl TmuxDomainState { active_lock: active_lock.clone(), master_pane: ref_pane, }; - let writer = pane_pty.try_clone_writer().unwrap(); + let writer = pane_pty.try_clone_writer()?; let mux = Mux::get().expect("should be called at main thread"); let size = PtySize { rows: pane.pane_height as u16, @@ -278,8 +278,11 @@ impl TmuxCommand for CapturePane { let pane_map = tmux_domain.inner.remote_panes.borrow(); if let Some(pane) = pane_map.get(&self.0) { - let lock = pane.lock().unwrap(); - lock.tx.send(result.output.to_owned()).unwrap(); + let lock = pane.lock().expect("Grant lock of tmux cmd queue failed"); + return lock + .tx + .send(result.output.to_owned()) + .map_err(|err| anyhow!("Send to tmux cmd queue failed {}", err)); } Ok(()) From d4b347077c6be84362ae224e4a5a5892db229896 Mon Sep 17 00:00:00 2001 From: g4c Date: Wed, 29 Sep 2021 18:14:41 +0800 Subject: [PATCH 21/21] buffer extra large message before write to mux --- mux/src/tmux_pty.rs | 46 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/mux/src/tmux_pty.rs b/mux/src/tmux_pty.rs index 0a016e71c38..7102a9a1090 100644 --- a/mux/src/tmux_pty.rs +++ b/mux/src/tmux_pty.rs @@ -10,16 +10,48 @@ use std::{ pub(crate) struct TmuxReader { rx: flume::Receiver, + // If a string received from rx is larger then the buffer to write out, + // we put that string here and use a cursor to indicate all chars before + // that cursor have been written out. + // Clear this buffer before receive next string. + head_buffer: String, + head_cursor: usize, // the first char of next write } impl Read for TmuxReader { fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { - match self.rx.recv() { - Ok(str) => { - return buf.write(str.as_bytes()); - } - Err(_) => { - return Ok(0); + if !self.head_buffer.is_empty() { + let mut buffer_cleared = false; + let bytes = if self.head_cursor + buf.len() >= self.head_buffer.len() { + buffer_cleared = true; + &self.head_buffer[self.head_cursor..] + } else { + &self.head_buffer[self.head_cursor..(self.head_cursor + buf.len())] + }; + return buf.write(bytes.as_bytes()).map(|res| { + // update buffer if write success + if buffer_cleared { + self.head_buffer.clear(); + self.head_cursor = 0; + } else { + self.head_cursor = self.head_cursor + buf.len(); + } + res + }); + } else { + match self.rx.recv() { + Ok(str) => { + if str.len() > buf.len() { + self.head_buffer = str; + self.head_cursor = 0; + return self.read(buf); + } else { + return buf.write(str.as_bytes()); + } + } + Err(_) => { + return Ok(0); + } } } } @@ -105,6 +137,8 @@ impl MasterPty for TmuxPty { fn try_clone_reader(&self) -> Result, anyhow::Error> { Ok(Box::new(TmuxReader { rx: self.rx.clone(), + head_buffer: String::default(), + head_cursor: 0, })) }