Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Tmux control mode #1090

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions mux/Cargo.toml
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion mux/src/lib.rs
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down
15 changes: 9 additions & 6 deletions mux/src/localpane.rs
Expand Up @@ -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);
}
}
_ => {
Expand Down
232 changes: 113 additions & 119 deletions mux/src/tmux.rs
@@ -1,15 +1,16 @@
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 flume;
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)]
Expand All @@ -19,156 +20,120 @@ 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<String>,
pub active_lock: Arc<(Mutex<bool>, 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<Mutex<TmuxRemotePane>>;

log::error!("panes in domain_id {}: {:?}", domain_id, items);
Ok(())
}
pub(crate) struct TmuxTab {
pub tab_id: TabId, // local tab ID
pub tmux_window_id: TmuxWindowId,
pub panes: HashSet<TmuxPaneId>, // tmux panes within tmux window
}

pub(crate) type TmuxCmdQueue = VecDeque<Box<dyn TmuxCommand>>;
pub(crate) struct TmuxDomainState {
pane_id: PaneId,
pub pane_id: PaneId,
pub domain_id: DomainId,
parser: RefCell<Parser>,
// parser: RefCell<Parser>,
state: RefCell<State>,
cmd_queue: RefCell<VecDeque<Box<dyn TmuxCommand>>>,
pub cmd_queue: Arc<Mutex<TmuxCmdQueue>>,
pub gui_window: RefCell<Option<MuxWindowBuilder>>,
pub gui_tabs: RefCell<Vec<TmuxTab>>,
pub remote_panes: RefCell<HashMap<TmuxPaneId, RefTmuxRemotePane>>,
pub tmux_session: RefCell<Option<TmuxSessionId>>,
}

pub struct TmuxDomain {
pub(crate) inner: Arc<TmuxDomainState>,
}

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<Vec<Event>>) {
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::<TmuxDomain>() {
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::kick_off(self.domain_id);
}
}

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);
let mux = Mux::get().expect("to be called on main thread");
Expand All @@ -179,21 +144,50 @@ impl TmuxDomainState {
*self.state.borrow_mut() = State::WaitingForResponse;
}
}

pub fn kick_off(domain_id: usize) {
Dixeran marked this conversation as resolved.
Show resolved Hide resolved
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::<TmuxDomain>() {
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");
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::<Box<dyn TmuxCommand>>::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 }
}

Expand Down