1
1
mirror of https://github.com/wez/wezterm.git synced 2024-08-16 09:40:34 +03:00

Fix interaction between pane swapping / rotating and client domains.

This should address #4200.

Currently pane swapping and rotation only works correctly on local
domains since none of the state is propagated to the remote mux server.

This patch adds a couple of RPCs for rotating panes and for swapping
an active pane with the pane at a given index. Additionally, it renames
the corresponding methods on the `mux::tab` module, prefixing them with
`local_`, adds `remote_` versions of them to Domains and adds a
convenience method on `mux`, mirroring the pattern used for
`move_pane_to_new_tab`, which dispatches to the relevant method.

This incidentally fixes a typo in the Lua API which was previously
always rotating panes in a single direction.
This commit is contained in:
Bogdan-Cristian Tătăroiu 2024-02-13 10:23:31 +00:00
parent b8f94c474c
commit 0f365952a1
15 changed files with 305 additions and 47 deletions

1
Cargo.lock generated
View File

@ -3214,6 +3214,7 @@ dependencies = [
"mux",
"parking_lot 0.12.2",
"portable-pty",
"promise",
"smol",
"termwiz",
"termwiz-funcs",

View File

@ -12,7 +12,7 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
use anyhow::{bail, Context as _, Error};
use config::keyassignment::{PaneDirection, ScrollbackEraseMode};
use config::keyassignment::{PaneDirection, RotationDirection, ScrollbackEraseMode};
use mux::client::{ClientId, ClientInfo};
use mux::pane::PaneId;
use mux::renderable::{RenderableDimensions, StableCursorPosition};
@ -493,7 +493,7 @@ pdu! {
GetPaneRenderableDimensions: 51,
GetPaneRenderableDimensionsResponse: 52,
PaneFocused: 53,
TabResized: 54,
TabReflowed: 54,
TabAddedToWindow: 55,
TabTitleChanged: 56,
WindowTitleChanged: 57,
@ -502,6 +502,8 @@ pdu! {
GetPaneDirection: 60,
GetPaneDirectionResponse: 61,
AdjustPaneSize: 62,
RotatePanes: 63,
SwapActivePaneWithIndex: 64,
}
impl Pdu {
@ -803,7 +805,7 @@ pub struct TabAddedToWindow {
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct TabResized {
pub struct TabReflowed {
pub tab_id: TabId,
}
@ -887,6 +889,19 @@ pub struct ActivatePaneDirection {
pub direction: PaneDirection,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct RotatePanes {
pub pane_id: PaneId,
pub direction: RotationDirection,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct SwapActivePaneWithIndex {
pub active_pane_id: PaneId,
pub with_pane_index: usize,
pub keep_focus: bool,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct GetPaneRenderChanges {
pub pane_id: PaneId,

View File

@ -641,7 +641,7 @@ impl Default for SplitSize {
}
}
#[derive(Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, FromDynamic, ToDynamic)]
pub enum RotationDirection {
Clockwise,
CounterClockwise,

View File

@ -16,6 +16,7 @@ log = "0.4"
luahelper = { path = "../../luahelper" }
parking_lot = "0.12"
portable-pty = { path = "../../pty" }
promise = { path = "../../promise" }
smol = "2.0"
termwiz = { path = "../../termwiz" }
termwiz-funcs = { path = "../termwiz-funcs" }

View File

@ -1,4 +1,4 @@
use config::keyassignment::PaneDirection;
use config::keyassignment::{PaneDirection, RotationDirection};
use super::*;
use luahelper::mlua::Value;
@ -113,14 +113,34 @@ impl UserData for MuxTab {
methods.add_method("rotate_counter_clockwise", |_, this, _: ()| {
let mux = get_mux()?;
let tab = this.resolve(&mux)?;
tab.rotate_counter_clockwise();
let tab_id = tab.tab_id();
let direction = RotationDirection::CounterClockwise;
promise::spawn::spawn(async move {
let mux = Mux::get();
if let Err(err) = mux.rotate_panes(tab_id, direction).await {
log::error!("Unable to rotate panes: {:#}", err);
}
})
.detach();
Ok(())
});
methods.add_method("rotate_clockwise", |_, this, _: ()| {
let mux = get_mux()?;
let tab = this.resolve(&mux)?;
tab.rotate_counter_clockwise();
let tab_id = tab.tab_id();
let direction = RotationDirection::CounterClockwise;
promise::spawn::spawn(async move {
let mux = Mux::get();
if let Err(err) = mux.rotate_panes(tab_id, direction).await {
log::error!("Unable to rotate panes: {:#}", err);
}
})
.detach();
Ok(())
});

View File

@ -12,7 +12,7 @@ use crate::window::WindowId;
use crate::Mux;
use anyhow::{bail, Context, Error};
use async_trait::async_trait;
use config::keyassignment::{SpawnCommand, SpawnTabDomain};
use config::keyassignment::{RotationDirection, SpawnCommand, SpawnTabDomain};
use config::{configuration, ExecDomain, SerialDomain, ValueOrFunc, WslDomain};
use downcast_rs::{impl_downcast, Downcast};
use parking_lot::Mutex;
@ -142,7 +142,7 @@ pub trait Domain: Downcast + Send + Sync {
/// is being moved to give the domain a chance to handle the movement.
/// If this method returns Ok(None), then the mux will handle the
/// movement itself by mutating its local Tabs and Windows.
async fn move_pane_to_new_tab(
async fn remote_move_pane_to_new_tab(
&self,
_pane_id: PaneId,
_window_id: Option<WindowId>,
@ -151,6 +151,31 @@ pub trait Domain: Downcast + Send + Sync {
Ok(None)
}
/// The mux will call this method on the domain of the panes that are being
/// rotated to give the domain a chance to handle the movement. If this
/// method returns Ok(false), then the mux will handle the movement itself
/// by mutating its local Tabs and Windows.
async fn remote_rotate_panes(
&self,
_pane_id: PaneId,
_direction: RotationDirection,
) -> anyhow::Result<bool> {
Ok(false)
}
/// The mux will call this method on the domain of the pane that is being
/// swapped to give the domain a chance to handle the movement. If this
/// method returns Ok(false), then the mux will handle the movement itself
/// by mutating its local Tabs and Windows.
async fn remote_swap_active_pane_with_index(
&self,
_active_pane_id: PaneId,
_with_pane_index: usize,
_keep_focus: bool,
) -> anyhow::Result<bool> {
Ok(false)
}
/// Returns false if the `spawn` method will never succeed.
/// There are some internal placeholder domains that are
/// pre-created with local UI that we do not want to allow

View File

@ -4,7 +4,7 @@ use crate::ssh_agent::AgentProxy;
use crate::tab::{SplitRequest, Tab, TabId};
use crate::window::{Window, WindowId};
use anyhow::{anyhow, Context, Error};
use config::keyassignment::SpawnTabDomain;
use config::keyassignment::{RotationDirection, SpawnTabDomain};
use config::{configuration, ExitBehavior, GuiPosition};
use domain::{Domain, DomainId, DomainState, SplitSource};
use filedescriptor::{poll, pollfd, socketpair, AsRawSocketDescriptor, FileDescriptor, POLLIN};
@ -80,7 +80,7 @@ pub enum MuxNotification {
window_id: WindowId,
},
PaneFocused(PaneId),
TabResized(TabId),
TabReflowed(TabId),
TabTitleChanged {
tab_id: TabId,
title: String,
@ -1245,7 +1245,7 @@ impl Mux {
.ok_or_else(|| anyhow::anyhow!("domain {domain_id} of pane {pane_id} not found"))?;
if let Some((tab, window_id)) = domain
.move_pane_to_new_tab(pane_id, window_id, workspace_for_new_window.clone())
.remote_move_pane_to_new_tab(pane_id, window_id, workspace_for_new_window.clone())
.await?
{
return Ok((tab, window_id));
@ -1289,6 +1289,71 @@ impl Mux {
Ok((tab, window_id))
}
pub async fn rotate_panes(
&self,
tab_id: TabId,
direction: RotationDirection,
) -> anyhow::Result<()> {
let tab = match self.get_tab(tab_id) {
Some(tab) => tab,
None => anyhow::bail!("Invalid tab id {}", tab_id),
};
// This makes the assumption that a tab contains only panes from a single local domain,
// though that is also an assumption that ClientDomain makes when syncing tab panes.
let tab_panes = tab.iter_panes();
let pos_pane = match tab_panes.iter().nth(0) {
Some(pos_pane) => pos_pane,
None => anyhow::bail!("Tab contains no panes: {}", tab_id),
};
let pane_id = pos_pane.pane.pane_id();
let domain_id = pos_pane.pane.domain_id();
let domain = self
.get_domain(domain_id)
.ok_or_else(|| anyhow::anyhow!("domain {domain_id} of tab {tab_id} not found"))?;
if domain.remote_rotate_panes(pane_id, direction).await? {
return Ok(());
}
match direction {
RotationDirection::Clockwise => tab.local_rotate_clockwise(),
RotationDirection::CounterClockwise => tab.local_rotate_counter_clockwise(),
}
Ok(())
}
pub async fn swap_active_pane_with_index(
&self,
active_pane_id: PaneId,
with_pane_index: usize,
keep_focus: bool,
) -> anyhow::Result<()> {
let (domain_id, _window_id, tab_id) = self
.resolve_pane_id(active_pane_id)
.ok_or_else(|| anyhow::anyhow!("pane {} not found", active_pane_id))?;
let domain = self.get_domain(domain_id).ok_or_else(|| {
anyhow::anyhow!("domain {domain_id} of pane {active_pane_id} not found")
})?;
if domain
.remote_swap_active_pane_with_index(active_pane_id, with_pane_index, keep_focus)
.await?
{
return Ok(());
}
let tab = match self.get_tab(tab_id) {
Some(tab) => tab,
None => anyhow::bail!("Invalid tab id {}", tab_id),
};
tab.local_swap_active_with_index(with_pane_index, keep_focus);
Ok(())
}
pub async fn spawn_tab_or_window(
&self,
window_id: Option<WindowId>,

View File

@ -581,12 +581,12 @@ impl Tab {
self.inner.lock().iter_panes_ignoring_zoom()
}
pub fn rotate_counter_clockwise(&self) {
self.inner.lock().rotate_counter_clockwise()
pub fn local_rotate_counter_clockwise(&self) {
self.inner.lock().local_rotate_counter_clockwise()
}
pub fn rotate_clockwise(&self) {
self.inner.lock().rotate_clockwise()
pub fn local_rotate_clockwise(&self) {
self.inner.lock().local_rotate_clockwise()
}
pub fn iter_splits(&self) -> Vec<PositionedSplit> {
@ -714,10 +714,10 @@ impl Tab {
}
/// Swap the active pane with the specified pane_index
pub fn swap_active_with_index(&self, pane_index: usize, keep_focus: bool) -> Option<()> {
pub fn local_swap_active_with_index(&self, pane_index: usize, keep_focus: bool) -> Option<()> {
self.inner
.lock()
.swap_active_with_index(pane_index, keep_focus)
.local_swap_active_with_index(pane_index, keep_focus)
}
/// Computes the size of the pane that would result if the specified
@ -908,7 +908,7 @@ impl TabInner {
self.zoomed.replace(pane);
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn contains_pane(&self, pane: PaneId) -> bool {
@ -937,7 +937,7 @@ impl TabInner {
self.iter_panes_impl(false)
}
fn rotate_counter_clockwise(&mut self) {
fn local_rotate_counter_clockwise(&mut self) {
let panes = self.iter_panes_ignoring_zoom();
if panes.is_empty() {
// Shouldn't happen, but we check for this here so that the
@ -966,9 +966,10 @@ impl TabInner {
}
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn rotate_clockwise(&mut self) {
fn local_rotate_clockwise(&mut self) {
let panes = self.iter_panes_ignoring_zoom();
if panes.is_empty() {
// Shouldn't happen, but we check for this here so that the
@ -997,7 +998,7 @@ impl TabInner {
}
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn iter_panes_impl(&mut self, respect_zoom_state: bool) -> Vec<PositionedPane> {
@ -1179,7 +1180,7 @@ impl TabInner {
apply_sizes_from_splits(self.pane.as_mut().unwrap(), &size);
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn apply_pane_size(&mut self, pane_size: TerminalSize, cursor: &mut Cursor) {
@ -1255,7 +1256,7 @@ impl TabInner {
self.size = size;
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn resize_split_by(&mut self, split_index: usize, delta: isize) {
@ -1288,7 +1289,7 @@ impl TabInner {
// Now cursor is looking at the split
self.adjust_node_at_cursor(&mut cursor, delta);
self.cascade_size_from_cursor(cursor);
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn adjust_node_at_cursor(&mut self, cursor: &mut Cursor, delta: isize) {
@ -1371,7 +1372,7 @@ impl TabInner {
}
}
}
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabResized(self.id)));
Mux::try_get().map(|mux| mux.notify(MuxNotification::TabReflowed(self.id)));
}
fn adjust_pane_size(&mut self, direction: PaneDirection, amount: usize) {
@ -1807,7 +1808,7 @@ impl TabInner {
cell_dimensions(&self.size)
}
fn swap_active_with_index(&mut self, pane_index: usize, keep_focus: bool) -> Option<()> {
fn local_swap_active_with_index(&mut self, pane_index: usize, keep_focus: bool) -> Option<()> {
let active_idx = self.get_active_idx();
let mut pane = self.get_active_pane()?;
log::trace!(

View File

@ -297,7 +297,7 @@ fn process_unilateral(
.detach();
return Ok(());
}
Pdu::TabResized(_) | Pdu::TabAddedToWindow(_) => {
Pdu::TabReflowed(_) | Pdu::TabAddedToWindow(_) => {
log::trace!("resync due to {:?}", decoded.pdu);
promise::spawn::spawn_into_main_thread(async move {
let mux = Mux::try_get().ok_or_else(|| anyhow!("no more mux"))?;
@ -1354,6 +1354,12 @@ impl Client {
rpc!(resize, Resize, UnitResponse);
rpc!(set_zoomed, SetPaneZoomed, UnitResponse);
rpc!(activate_pane_direction, ActivatePaneDirection, UnitResponse);
rpc!(
swap_active_pane_with_index,
SwapActivePaneWithIndex,
UnitResponse
);
rpc!(rotate_panes, RotatePanes, UnitResponse);
rpc!(
get_pane_render_changes,
GetPaneRenderChanges,

View File

@ -3,7 +3,7 @@ use crate::pane::ClientPane;
use anyhow::{anyhow, bail};
use async_trait::async_trait;
use codec::{ListPanesResponse, SpawnV2, SplitPane};
use config::keyassignment::SpawnTabDomain;
use config::keyassignment::{RotationDirection, SpawnTabDomain};
use config::{SshDomain, TlsDomainClient, UnixDomain};
use mux::connui::{ConnectionUI, ConnectionUIParams};
use mux::domain::{alloc_domain_id, Domain, DomainId, DomainState, SplitSource};
@ -763,7 +763,7 @@ impl Domain for ClientDomain {
/// Forward the request to the remote; we need to translate the local ids
/// to those that match the remote for the request, resync the changed
/// structure, and then translate the results back to local
async fn move_pane_to_new_tab(
async fn remote_move_pane_to_new_tab(
&self,
pane_id: PaneId,
window_id: Option<WindowId>,
@ -814,6 +814,64 @@ impl Domain for ClientDomain {
Ok(Some((tab, local_win_id)))
}
async fn remote_rotate_panes(
&self,
pane_id: PaneId,
direction: RotationDirection,
) -> anyhow::Result<bool> {
let inner = self
.inner()
.ok_or_else(|| anyhow!("domain is not attached"))?;
let local_pane = Mux::get()
.get_pane(pane_id)
.ok_or_else(|| anyhow!("pane_id {} is invalid", pane_id))?;
let pane = local_pane
.downcast_ref::<ClientPane>()
.ok_or_else(|| anyhow!("pane_id {} is not a ClientPane", pane_id))?;
inner
.client
.rotate_panes(codec::RotatePanes {
pane_id: pane.remote_pane_id,
direction,
})
.await?;
self.resync().await?;
Ok(true)
}
async fn remote_swap_active_pane_with_index(
&self,
active_pane_id: PaneId,
with_pane_index: usize,
keep_focus: bool,
) -> anyhow::Result<bool> {
let inner = self
.inner()
.ok_or_else(|| anyhow!("domain is not attached"))?;
let local_pane = Mux::get()
.get_pane(active_pane_id)
.ok_or_else(|| anyhow!("pane_id {} is invalid", active_pane_id))?;
let pane = local_pane
.downcast_ref::<ClientPane>()
.ok_or_else(|| anyhow!("pane_id {} is not a ClientPane", active_pane_id))?;
inner
.client
.swap_active_pane_with_index(codec::SwapActivePaneWithIndex {
active_pane_id: pane.remote_pane_id,
with_pane_index,
keep_focus,
})
.await?;
self.resync().await?;
Ok(true)
}
async fn spawn(
&self,
size: TerminalSize,

View File

@ -88,7 +88,7 @@ impl GuiFrontEnd {
}
MuxNotification::TabTitleChanged { .. } => {}
MuxNotification::WindowTitleChanged { .. } => {}
MuxNotification::TabResized(_) => {}
MuxNotification::TabReflowed(_) => {}
MuxNotification::TabAddedToWindow { .. } => {}
MuxNotification::PaneRemoved(_) => {}
MuxNotification::WindowInvalidated(_) => {}

View File

@ -30,8 +30,8 @@ use ::wezterm_term::input::{ClickPosition, MouseButton as TMB};
use ::window::*;
use anyhow::{anyhow, ensure, Context};
use config::keyassignment::{
KeyAssignment, PaneDirection, Pattern, PromptInputLine, QuickSelectArguments,
RotationDirection, SpawnCommand, SplitSize,
KeyAssignment, PaneDirection, Pattern, PromptInputLine, QuickSelectArguments, SpawnCommand,
SplitSize,
};
use config::window::WindowLevel;
use config::{
@ -1292,7 +1292,7 @@ impl TermWindow {
// Also handled by clientpane
self.update_title_post_status();
}
MuxNotification::TabResized(_) => {
MuxNotification::TabReflowed(_) => {
// Also handled by wezterm-client
self.update_title_post_status();
}
@ -1489,7 +1489,7 @@ impl TermWindow {
return true;
}
}
MuxNotification::TabResized(tab_id)
MuxNotification::TabReflowed(tab_id)
| MuxNotification::TabTitleChanged { tab_id, .. } => {
let mux = Mux::get();
if mux.window_containing_tab(tab_id) == Some(mux_window_id) {
@ -3013,10 +3013,15 @@ impl TermWindow {
Some(tab) => tab,
None => return Ok(PerformAssignmentResult::Handled),
};
match direction {
RotationDirection::Clockwise => tab.rotate_clockwise(),
RotationDirection::CounterClockwise => tab.rotate_counter_clockwise(),
}
let tab_id = tab.tab_id();
let direction = *direction;
promise::spawn::spawn(async move {
let mux = Mux::get();
if let Err(err) = mux.rotate_panes(tab_id, direction).await {
log::error!("Unable to rotate panes: {:#}", err);
}
})
.detach()
}
SplitPane(split) => {
log::trace!("SplitPane {:?}", split);

View File

@ -180,17 +180,26 @@ impl PaneSelector {
}
}
PaneSelectMode::SwapWithActiveKeepFocus | PaneSelectMode::SwapWithActive => {
tab.swap_active_with_index(
pane_index,
self.mode == PaneSelectMode::SwapWithActiveKeepFocus,
);
if let Some(active_pane) = tab.get_active_pane() {
let active_pane_id = active_pane.pane_id();
let keep_focus = self.mode == PaneSelectMode::SwapWithActiveKeepFocus;
promise::spawn::spawn(async move {
if let Err(err) = mux
.swap_active_pane_with_index(active_pane_id, pane_index, keep_focus)
.await
{
log::error!("failed to swap_active_pane_with_index: {err:#}");
}
})
.detach();
}
}
PaneSelectMode::MoveToNewWindow => {
if let Some(pos) = panes.iter().find(|p| p.index == pane_index) {
let pane_id = pos.pane.pane_id();
promise::spawn::spawn(async move {
if let Err(err) = mux.move_pane_to_new_tab(pane_id, None, None).await {
log::error!("failed to move_pane_to_new_tab: {err:#}");
log::error!("failed to move_pane_to_new_window: {err:#}");
}
})
.detach();

View File

@ -172,8 +172,8 @@ where
.await?;
stream.flush().await.context("flushing PDU to client")?;
}
Ok(Item::Notif(MuxNotification::TabResized(tab_id))) => {
Pdu::TabResized(codec::TabResized { tab_id })
Ok(Item::Notif(MuxNotification::TabReflowed(tab_id))) => {
Pdu::TabReflowed(codec::TabReflowed { tab_id })
.encode_async(&mut stream, 0)
.await?;
stream.flush().await.context("flushing PDU to client")?;

View File

@ -1,6 +1,7 @@
use crate::PKI;
use anyhow::{anyhow, Context};
use codec::*;
use config::keyassignment::RotationDirection;
use config::TermConfig;
use mux::client::ClientId;
use mux::domain::SplitSource;
@ -629,6 +630,57 @@ impl SessionHandler {
.detach();
}
Pdu::SwapActivePaneWithIndex(SwapActivePaneWithIndex {
active_pane_id,
with_pane_index,
keep_focus,
}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get();
let (_domain_id, _window_id, tab_id) = mux
.resolve_pane_id(active_pane_id)
.ok_or_else(|| anyhow!("no such pane {}", active_pane_id))?;
let tab = mux
.get_tab(tab_id)
.ok_or_else(|| anyhow!("no such tab {}", tab_id))?;
tab.local_swap_active_with_index(with_pane_index, keep_focus);
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::RotatePanes(RotatePanes { pane_id, direction }) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get();
let (_domain_id, _window_id, tab_id) = mux
.resolve_pane_id(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
let tab = mux
.get_tab(tab_id)
.ok_or_else(|| anyhow!("no such tab {}", tab_id))?;
match direction {
RotationDirection::Clockwise => tab.local_rotate_clockwise(),
RotationDirection::CounterClockwise => {
tab.local_rotate_counter_clockwise()
}
};
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::Resize(Resize {
containing_tab_id,
pane_id,
@ -1004,7 +1056,7 @@ impl SessionHandler {
| Pdu::GetClientListResponse { .. }
| Pdu::PaneRemoved { .. }
| Pdu::PaneFocused { .. }
| Pdu::TabResized { .. }
| Pdu::TabReflowed { .. }
| Pdu::GetImageCellResponse { .. }
| Pdu::MovePaneToNewTabResponse { .. }
| Pdu::TabAddedToWindow { .. }