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/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/lib.rs b/mux/src/lib.rs index 73c58bf8e54..f07b2e6353d 100644 --- a/mux/src/lib.rs +++ b/mux/src/lib.rs @@ -34,6 +34,8 @@ pub mod ssh; pub mod tab; pub mod termwiztermtab; pub mod tmux; +pub mod tmux_commands; +mod tmux_pty; pub mod window; use crate::activity::Activity; @@ -491,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/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); } } _ => { diff --git a/mux/src/tmux.rs b/mux/src/tmux.rs index 100c0f5c089..5f941e32c21 100644 --- a/mux/src/tmux.rs +++ b/mux/src/tmux.rs @@ -1,15 +1,15 @@ 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 crate::{Mux, MuxWindowBuilder}; use async_trait::async_trait; use portable_pty::{CommandBuilder, PtySize}; use std::cell::RefCell; -use std::collections::VecDeque; +use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; -use std::sync::Arc; +use std::sync::{Arc, Condvar, Mutex}; use tmux_cc::*; #[derive(PartialEq, Eq, Debug, Copy, Clone)] @@ -19,105 +19,44 @@ enum State { WaitingForResponse, } -trait TmuxCommand { - fn get_command(&self) -> String; - fn process_result(&self, domain_id: DomainId, result: &Guarded) -> anyhow::Result<()>; +#[derive(Debug)] +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, + pub 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, } -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, - }); - } +pub(crate) type RefTmuxRemotePane = Arc>; - log::error!("panes in domain_id {}: {:?}", domain_id, items); - Ok(()) - } +/// 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, + pub panes: HashSet, // tmux panes within tmux window } +pub(crate) type TmuxCmdQueue = VecDeque>; pub(crate) struct TmuxDomainState { - 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, - cmd_queue: RefCell>>, + pub cmd_queue: Arc>, + pub gui_window: RefCell>, + pub gui_tabs: RefCell>, + pub remote_panes: RefCell>, + pub tmux_session: RefCell>, } pub struct TmuxDomain { @@ -125,52 +64,80 @@ 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 { + log::info!("tmux: {:?} in state {:?}", event, state); + match event { + Event::Guarded(response) => match state { State::WaitForInitialGuard => { *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(); promise::spawn::spawn(async move { - if let Err(err) = cmd.process_result(domain_id, &response) { - log::error!("error processing result: {}", err); + if let Err(err) = cmd.process_result(domain_id, &resp) { + log::error!("Tmux processing command result error: {}", err); } }) .detach(); } State::Idle => {} + }, + Event::Output { pane, text } => { + let pane_map = self.remote_panes.borrow_mut(); + if let Some(ref_pane) = pane_map.get(pane) { + 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); + } } - } - } - 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(); + Event::WindowAdd { window: _ } => { + self.create_gui_window(); + } + Event::SessionChanged { session, name: _ } => { + *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(); } } - }) - .detach(); + _ => {} + } + } + + // 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::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; } - 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); + 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(); @@ -179,21 +146,52 @@ impl TmuxDomainState { *self.state.borrow_mut() = State::WaitingForResponse; } } + + /// 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) { + if let Some(tmux_domain) = domain.downcast_ref::() { + tmux_domain.send_next_command(); + } + } + }) + .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"); + 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 { 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), + 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 new file mode 100644 index 00000000000..3c9260e5f59 --- /dev/null +++ b/mux/src/tmux_commands.rs @@ -0,0 +1,309 @@ +use crate::domain::DomainId; +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::fmt::Debug; +use std::fmt::Write; +use std::rc::Rc; +use std::sync::{Arc, Condvar, Mutex}; +use tmux_cc::*; + +pub(crate) trait TmuxCommand: Send + Debug { + fn get_command(&self) -> String; + 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, +} + +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 + .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; + } + } + } + + /// 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 + .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 + // 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 || self.check_pane_attached(&pane) { + continue; + } + + 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, + 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 { + domain_id: self.domain_id, + rx: channel.1.clone(), + cmd_queue: self.cmd_queue.clone(), + active_lock: active_lock.clone(), + master_pane: ref_pane, + }; + 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, + 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); + + self.create_gui_window(); + let mut gui_window = self.gui_window.borrow_mut(); + let gui_window_id = match gui_window.as_mut() { + Some(x) => x, + None => { + anyhow::bail!("No tmux gui 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())?; + log::info!("new pane attached"); + } + Ok(()) + } +} + +#[derive(Debug)] +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<()> { + 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(PaneItem { + session_id, + window_id, + pane_id, + pane_index, + cursor_x, + cursor_y, + pane_width, + pane_height, + pane_left, + pane_top, + }); + } + + 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"); + } +} + +#[derive(Debug)] +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().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(()) + } +} + +#[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} ", byte).expect("unable to write key"); + } + format!("send-keys -t {} {}\r", self.pane, s) + } + + 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 new file mode 100644 index 00000000000..7102a9a1090 --- /dev/null +++ b/mux/src/tmux_pty.rs @@ -0,0 +1,159 @@ +use crate::{ + tmux::{RefTmuxRemotePane, TmuxCmdQueue, TmuxDomainState}, + tmux_commands::SendKeys, +}; +use portable_pty::{Child, ExitStatus, MasterPty}; +use std::{ + io::{Read, Write}, + sync::{Arc, Condvar, Mutex}, +}; + +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 { + 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); + } + } + } + } +} + +/// 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)>, +} + +impl Write for TmuxPty { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let pane_id = { + let pane_lock = self.master_pane.lock().unwrap(); + pane_lock.pane_id + }; + 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, + keys: buf.to_vec(), + })); + TmuxDomainState::schedule_send_next_command(self.domain_id); + Ok(0) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +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 { + 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 { + Some(0) + } + + #[cfg(windows)] + fn as_raw_handle(&self) -> Option { + None + } +} + +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(), + head_buffer: String::default(), + head_cursor: 0, + })) + } + + 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(), + })) + } + + #[cfg(unix)] + fn process_group_leader(&self) -> Option { + return None; + } +} 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..767e5c1e2ee 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::Event; 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 @@ -80,12 +83,43 @@ 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 mut perform = Performer { - callback: &mut callback, - state: &mut self.state.borrow_mut(), - }; - self.state_machine.parse(bytes, &mut perform); + let is_tmux_mode: bool = self.state.borrow().tmux_state.is_some(); + if is_tmux_mode { + 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); + } + } + } 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 +249,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 +272,24 @@ 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(); + 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(_) => { + 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..631da97e43a 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) => { + // concat remained bytes after digested bytes + return Err(anyhow::anyhow!(format!( + "{}{}", + err, + String::from_utf8_lossy(&bytes[i..]) + ))); + } } } - 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,