1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 05:12:40 +03:00

Add SplitPane assignment

This, along with the plumbing included here, allows specifying
the destination of the split (now you can specify top/left, whereas
previously it was limited to right/bottom), as well as the size
of the split, and also whether the split targets the node at the
top level of the tab rather than the active pane--that is referred
to as full-width in tmux terminology.

https://github.com/wez/wezterm/issues/578
This commit is contained in:
Wez Furlong 2022-05-21 20:50:50 -07:00
parent d2d4257f79
commit ecd05547d5
15 changed files with 459 additions and 93 deletions

View File

@ -189,6 +189,14 @@ impl<L, N> Tree<L, N> {
path: Box::new(Path::Top), path: Box::new(Path::Top),
} }
} }
pub fn num_leaves(&self) -> usize {
match self {
Self::Empty => 0,
Self::Leaf(_) => 1,
Self::Node { left, right, .. } => left.num_leaves() + right.num_leaves(),
}
}
} }
impl<L, N> Cursor<L, N> { impl<L, N> Cursor<L, N> {
@ -313,6 +321,34 @@ impl<L, N> Cursor<L, N> {
} }
} }
pub fn split_node_and_insert_left(self, to_insert: L) -> Result<Self, Self> {
match *self.it {
Tree::Node { left, right, data } => Ok(Self {
it: Box::new(Tree::Node {
data: None,
right: Box::new(Tree::Node { left, right, data }),
left: Box::new(Tree::Leaf(to_insert)),
}),
path: self.path,
}),
_ => Err(self),
}
}
pub fn split_node_and_insert_right(self, to_insert: L) -> Result<Self, Self> {
match *self.it {
Tree::Node { left, right, data } => Ok(Self {
it: Box::new(Tree::Node {
data: None,
left: Box::new(Tree::Node { left, right, data }),
right: Box::new(Tree::Leaf(to_insert)),
}),
path: self.path,
}),
_ => Err(self),
}
}
/// If the current position is a leaf, split it into a Node where /// If the current position is a leaf, split it into a Node where
/// the left side holds the current leaf value and the right side /// the left side holds the current leaf value and the right side
/// holds the provided `right` value. /// holds the provided `right` value.

View File

@ -15,7 +15,7 @@ use anyhow::{bail, Context as _, Error};
use mux::client::{ClientId, ClientInfo}; use mux::client::{ClientId, ClientInfo};
use mux::pane::PaneId; use mux::pane::PaneId;
use mux::renderable::{RenderableDimensions, StableCursorPosition}; use mux::renderable::{RenderableDimensions, StableCursorPosition};
use mux::tab::{PaneNode, SerdeUrl, SplitDirection, TabId}; use mux::tab::{PaneNode, SerdeUrl, SplitRequest, TabId};
use mux::window::WindowId; use mux::window::WindowId;
use portable_pty::{CommandBuilder, PtySize}; use portable_pty::{CommandBuilder, PtySize};
use rangeset::*; use rangeset::*;
@ -416,7 +416,7 @@ macro_rules! pdu {
/// The overall version of the codec. /// The overall version of the codec.
/// This must be bumped when backwards incompatible changes /// This must be bumped when backwards incompatible changes
/// are made to the types and protocol. /// are made to the types and protocol.
pub const CODEC_VERSION: usize = 22; pub const CODEC_VERSION: usize = 23;
// Defines the Pdu enum. // Defines the Pdu enum.
// Each struct has an explicit identifying number. // Each struct has an explicit identifying number.
@ -592,7 +592,7 @@ pub struct ListPanesResponse {
#[derive(Deserialize, Serialize, PartialEq, Debug)] #[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct SplitPane { pub struct SplitPane {
pub pane_id: PaneId, pub pane_id: PaneId,
pub direction: SplitDirection, pub split_request: SplitRequest,
pub command: Option<CommandBuilder>, pub command: Option<CommandBuilder>,
pub command_dir: Option<String>, pub command_dir: Option<String>,
pub domain: config::keyassignment::SpawnTabDomain, pub domain: config::keyassignment::SpawnTabDomain,

View File

@ -371,9 +371,33 @@ pub enum KeyAssignment {
CopyMode(CopyModeAssignment), CopyMode(CopyModeAssignment),
RotatePanes(RotationDirection), RotatePanes(RotationDirection),
SplitPane(SplitPane),
} }
impl_lua_conversion_dynamic!(KeyAssignment); impl_lua_conversion_dynamic!(KeyAssignment);
#[derive(Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
pub struct SplitPane {
pub direction: PaneDirection,
#[dynamic(default)]
pub size: SplitSize,
#[dynamic(default)]
pub command: SpawnCommand,
#[dynamic(default)]
pub top_level: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
pub enum SplitSize {
Cells(usize),
Percent(u8),
}
impl Default for SplitSize {
fn default() -> Self {
Self::Percent(50)
}
}
#[derive(Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)] #[derive(Debug, Clone, PartialEq, Eq, FromDynamic, ToDynamic)]
pub enum RotationDirection { pub enum RotationDirection {
Clockwise, Clockwise,

View File

@ -35,6 +35,7 @@ As features stabilize some brief notes about them will accumulate here.
* [cell_width](config/lua/config/cell_width.md) option to adjust the horizontal spacing when the availble font stretches are insufficient. [#1979](https://github.com/wez/wezterm/issues/1979) * [cell_width](config/lua/config/cell_width.md) option to adjust the horizontal spacing when the availble font stretches are insufficient. [#1979](https://github.com/wez/wezterm/issues/1979)
* [min_scroll_bar_height](config/lua/config/min_scroll_bar_height.md) to control the minimum size of the scroll bar thumb [#1936](https://github.com/wez/wezterm/issues/1936) * [min_scroll_bar_height](config/lua/config/min_scroll_bar_height.md) to control the minimum size of the scroll bar thumb [#1936](https://github.com/wez/wezterm/issues/1936)
* [RotatePanes](config/lua/keyassignment/RotatePanes.md) key assignment for re-arranging the panes in a tab * [RotatePanes](config/lua/keyassignment/RotatePanes.md) key assignment for re-arranging the panes in a tab
* [SplitPane](config/lua/keyassignment/SplitPane.md) key assignment that allows specifying the size and location of the split, as well as top-level (full width/height) splits. `wezterm cli split-pane --help` shows equivalent options you can use from the cli. [#578](https://github.com/wez/wezterm/issues/578)
#### Updated #### Updated

View File

@ -33,3 +33,4 @@ return {
} }
``` ```
See also: [SplitPane](SplitPane.md).

View File

@ -0,0 +1,29 @@
# SplitPane
*Since: nightly builds only*
Splits the active pane in a particular direction, spawning a new command into the newly created pane.
This assignment has a number of fields that control the overall action:
* `direction` - can be one of `"Up"`, `"Down"`, `"Left"`, `"Right"`. Specifies where the new pane will end up. This field is required.
* `size` - controls the size of the new pane. Can be `{Cells=10}` to specify eg: 10 cells or `{Percent=50}` to specify 50% of the available space. If omitted, `{Percent=50}` is the default
* `command` - the [SpawnCommand](../SpawnCommand.md) that specifies what program to launch into the new pane. If omitted, the [default_prog](../config/default_prog.md) is used
* `top_level` - if set to `true`, rather than splitting the active pane, the split will be made at the root of the tab and effectively split the entire tab across the full extent possible. The default is `false`.
```lua
local wezterm = require 'wezterm';
return {
keys = {
-- This will create a new split and run the `top` program inside it
{key="%", mods="CTRL|SHIFT|ALT", action=wezterm.action{SplitPane={
direction="Left",
command={args={"top"}},
size={Percent=50},
}}},
}
}
```
See also: [SplitHorizontal](SplitHorizontal.md), [SplitVertical](SplitVertical.md) and `wezterm cli split-pane --help`.

View File

@ -33,3 +33,4 @@ return {
} }
``` ```
See also: [SplitPane](SplitPane.md).

View File

@ -7,7 +7,7 @@
use crate::localpane::LocalPane; use crate::localpane::LocalPane;
use crate::pane::{alloc_pane_id, Pane, PaneId}; use crate::pane::{alloc_pane_id, Pane, PaneId};
use crate::tab::{SplitDirection, Tab, TabId}; use crate::tab::{SplitRequest, Tab, TabId};
use crate::window::WindowId; use crate::window::WindowId;
use crate::Mux; use crate::Mux;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
@ -59,7 +59,7 @@ pub trait Domain: Downcast {
command_dir: Option<String>, command_dir: Option<String>,
tab: TabId, tab: TabId,
pane_id: PaneId, pane_id: PaneId,
direction: SplitDirection, split_request: SplitRequest,
) -> anyhow::Result<Rc<dyn Pane>> { ) -> anyhow::Result<Rc<dyn Pane>> {
let mux = Mux::get().unwrap(); let mux = Mux::get().unwrap();
let tab = match mux.get_tab(tab) { let tab = match mux.get_tab(tab) {
@ -76,7 +76,7 @@ pub trait Domain: Downcast {
None => anyhow::bail!("invalid pane id {}", pane_id), None => anyhow::bail!("invalid pane id {}", pane_id),
}; };
let split_size = match tab.compute_split_size(pane_index, direction) { let split_size = match tab.compute_split_size(pane_index, split_request) {
Some(s) => s, Some(s) => s,
None => anyhow::bail!("invalid pane index {}", pane_index), None => anyhow::bail!("invalid pane index {}", pane_index),
}; };
@ -85,7 +85,7 @@ pub trait Domain: Downcast {
.spawn_pane(split_size.second, command, command_dir) .spawn_pane(split_size.second, command, command_dir)
.await?; .await?;
tab.split_and_insert(pane_index, direction, Rc::clone(&pane))?; tab.split_and_insert(pane_index, split_request, Rc::clone(&pane))?;
Ok(pane) Ok(pane)
} }

View File

@ -1,6 +1,6 @@
use crate::client::{ClientId, ClientInfo}; use crate::client::{ClientId, ClientInfo};
use crate::pane::{Pane, PaneId}; use crate::pane::{Pane, PaneId};
use crate::tab::{SplitDirection, Tab, TabId}; use crate::tab::{SplitRequest, Tab, TabId};
use crate::window::{Window, WindowId}; use crate::window::{Window, WindowId};
use anyhow::{anyhow, Context, Error}; use anyhow::{anyhow, Context, Error};
use config::keyassignment::SpawnTabDomain; use config::keyassignment::SpawnTabDomain;
@ -941,7 +941,7 @@ impl Mux {
&self, &self,
// TODO: disambiguate with TabId // TODO: disambiguate with TabId
pane_id: PaneId, pane_id: PaneId,
direction: SplitDirection, request: SplitRequest,
command: Option<CommandBuilder>, command: Option<CommandBuilder>,
command_dir: Option<String>, command_dir: Option<String>,
domain: config::keyassignment::SpawnTabDomain, domain: config::keyassignment::SpawnTabDomain,
@ -966,7 +966,7 @@ impl Mux {
let cwd = self.resolve_cwd(command_dir, Some(Rc::clone(&current_pane))); let cwd = self.resolve_cwd(command_dir, Some(Rc::clone(&current_pane)));
let pane = domain let pane = domain
.split_pane(command, cwd, tab_id, pane_id, direction) .split_pane(command, cwd, tab_id, pane_id, request)
.await?; .await?;
if let Some(config) = term_config { if let Some(config) = term_config {
pane.set_config(config); pane.set_config(config);

View File

@ -79,6 +79,41 @@ pub struct SplitDirectionAndSize {
pub second: PtySize, pub second: PtySize,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum SplitSize {
Cells(usize),
Percent(u8),
}
impl Default for SplitSize {
fn default() -> Self {
Self::Percent(50)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub struct SplitRequest {
pub direction: SplitDirection,
/// Whether the newly created item will be in the second part
/// of the split (right/bottom)
pub target_is_second: bool,
/// Split across the top of the tab rather than the active pane
pub top_level: bool,
/// The size of the new item
pub size: SplitSize,
}
impl Default for SplitRequest {
fn default() -> Self {
Self {
direction: SplitDirection::Horizontal,
target_is_second: true,
top_level: false,
size: SplitSize::default(),
}
}
}
impl SplitDirectionAndSize { impl SplitDirectionAndSize {
fn top_of_second(&self) -> usize { fn top_of_second(&self) -> usize {
match self.direction { match self.direction {
@ -1443,36 +1478,74 @@ impl Tab {
pub fn compute_split_size( pub fn compute_split_size(
&self, &self,
pane_index: usize, pane_index: usize,
direction: SplitDirection, request: SplitRequest,
) -> Option<SplitDirectionAndSize> { ) -> Option<SplitDirectionAndSize> {
let cell_dims = self.cell_dimensions(); let cell_dims = self.cell_dimensions();
fn split_dimension(dim: usize, request: SplitRequest) -> (usize, usize) {
let target_size = match request.size {
SplitSize::Cells(n) => n,
SplitSize::Percent(n) => (dim * (n as usize)) / 100,
}
.max(1);
let remain = dim.saturating_sub(target_size + 1);
if request.target_is_second {
(remain, target_size)
} else {
(target_size, remain)
}
}
if request.top_level {
let size = self.size.borrow().clone();
let ((width1, width2), (height1, height2)) = match request.direction {
SplitDirection::Horizontal => (
split_dimension(size.cols as usize, request),
(size.rows as usize, size.rows as usize),
),
SplitDirection::Vertical => (
(size.cols as usize, size.cols as usize),
split_dimension(size.rows as usize, request),
),
};
return Some(SplitDirectionAndSize {
direction: request.direction,
first: PtySize {
rows: height1 as _,
cols: width1 as _,
pixel_height: cell_dims.pixel_height * height1 as u16,
pixel_width: cell_dims.pixel_width * width1 as u16,
},
second: PtySize {
rows: height2 as _,
cols: width2 as _,
pixel_height: cell_dims.pixel_height * height2 as u16,
pixel_width: cell_dims.pixel_width * width2 as u16,
},
});
}
// Ensure that we're not zoomed, otherwise we'll end up in // Ensure that we're not zoomed, otherwise we'll end up in
// a bogus split state (https://github.com/wez/wezterm/issues/723) // a bogus split state (https://github.com/wez/wezterm/issues/723)
self.set_zoomed(false); self.set_zoomed(false);
self.iter_panes().iter().nth(pane_index).map(|pos| { self.iter_panes().iter().nth(pane_index).map(|pos| {
fn split_dimension(dim: usize) -> (usize, usize) { let ((width1, width2), (height1, height2)) = match request.direction {
let halved = dim / 2; SplitDirection::Horizontal => (
if halved * 2 == dim { split_dimension(pos.width, request),
// Was an even size; we need to allow 1 cell to render (pos.height, pos.height),
// the split UI, so make the newly created leaf slightly ),
// smaller SplitDirection::Vertical => {
(halved, halved.saturating_sub(1)) ((pos.width, pos.width), split_dimension(pos.height, request))
} else {
(halved, halved)
} }
}
let ((width1, width2), (height1, height2)) = match direction {
SplitDirection::Horizontal => {
(split_dimension(pos.width), (pos.height, pos.height))
}
SplitDirection::Vertical => ((pos.width, pos.width), split_dimension(pos.height)),
}; };
SplitDirectionAndSize { SplitDirectionAndSize {
direction, direction: request.direction,
first: PtySize { first: PtySize {
rows: height1 as _, rows: height1 as _,
cols: width1 as _, cols: width1 as _,
@ -1496,7 +1569,7 @@ impl Tab {
pub fn split_and_insert( pub fn split_and_insert(
&self, &self,
pane_index: usize, pane_index: usize,
direction: SplitDirection, request: SplitRequest,
pane: Rc<dyn Pane>, pane: Rc<dyn Pane>,
) -> anyhow::Result<usize> { ) -> anyhow::Result<usize> {
if self.zoomed.borrow().is_some() { if self.zoomed.borrow().is_some() {
@ -1505,10 +1578,11 @@ impl Tab {
{ {
let split_info = self let split_info = self
.compute_split_size(pane_index, direction) .compute_split_size(pane_index, request)
.ok_or_else(|| { .ok_or_else(|| {
anyhow::anyhow!("invalid pane_index {}; cannot split!", pane_index) anyhow::anyhow!("invalid pane_index {}; cannot split!", pane_index)
})?; })?;
let tab_size = *self.size.borrow(); let tab_size = *self.size.borrow();
if split_info.first.rows == 0 if split_info.first.rows == 0
|| split_info.first.cols == 0 || split_info.first.cols == 0
@ -1529,9 +1603,53 @@ impl Tab {
anyhow::bail!("No space for split!"); anyhow::bail!("No space for split!");
} }
let needs_resize = if request.top_level {
self.pane.borrow().as_ref().unwrap().num_leaves() > 1
} else {
false
};
if needs_resize {
// Pre-emptively resize the tab contents down to
// match the target size; it's easier to reuse
// existing resize logic that way
if request.target_is_second {
self.resize(split_info.first.clone());
} else {
self.resize(split_info.second.clone());
}
}
let mut root = self.pane.borrow_mut(); let mut root = self.pane.borrow_mut();
let mut cursor = root.take().unwrap().cursor(); let mut cursor = root.take().unwrap().cursor();
if request.top_level && !cursor.is_leaf() {
let result = if request.target_is_second {
cursor.split_node_and_insert_right(Rc::clone(&pane))
} else {
cursor.split_node_and_insert_left(Rc::clone(&pane))
};
cursor = match result {
Ok(c) => {
cursor = match c.assign_node(Some(split_info)) {
Err(c) | Ok(c) => c,
};
root.replace(cursor.tree());
let pane_index = if request.target_is_second {
root.as_ref().unwrap().num_leaves().saturating_sub(1)
} else {
0
};
*self.active.borrow_mut() = pane_index;
return Ok(pane_index);
}
Err(cursor) => cursor,
};
}
match cursor.go_to_nth_leaf(pane_index) { match cursor.go_to_nth_leaf(pane_index) {
Ok(c) => cursor = c, Ok(c) => cursor = c,
Err(c) => { Err(c) => {
@ -1542,10 +1660,18 @@ impl Tab {
let existing_pane = Rc::clone(cursor.leaf_mut().unwrap()); let existing_pane = Rc::clone(cursor.leaf_mut().unwrap());
existing_pane.resize(split_info.first)?; let (pane1, pane2) = if request.target_is_second {
pane.resize(split_info.second.clone())?; (existing_pane, pane)
} else {
(pane, existing_pane)
};
match cursor.split_leaf_and_insert_right(pane) { pane1.resize(split_info.first)?;
pane2.resize(split_info.second.clone())?;
*cursor.leaf_mut().unwrap() = pane1;
match cursor.split_leaf_and_insert_right(pane2) {
Ok(c) => cursor = c, Ok(c) => cursor = c,
Err(c) => { Err(c) => {
root.replace(c.tree()); root.replace(c.tree());
@ -1559,13 +1685,19 @@ impl Tab {
Err(c) | Ok(c) => root.replace(c.tree()), Err(c) | Ok(c) => root.replace(c.tree()),
}; };
if request.target_is_second {
*self.active.borrow_mut() = pane_index + 1; *self.active.borrow_mut() = pane_index + 1;
} }
}
log::debug!("split info after split: {:#?}", self.iter_splits()); log::debug!("split info after split: {:#?}", self.iter_splits());
log::debug!("pane info after split: {:#?}", self.iter_panes()); log::debug!("pane info after split: {:#?}", self.iter_panes());
Ok(pane_index + 1) Ok(if request.target_is_second {
pane_index + 1
} else {
pane_index
})
} }
} }
@ -1784,23 +1916,35 @@ mod test {
assert_eq!(24, panes[0].height); assert_eq!(24, panes[0].height);
assert!(tab assert!(tab
.compute_split_size(1, SplitDirection::Horizontal) .compute_split_size(
1,
SplitRequest {
direction: SplitDirection::Horizontal,
..Default::default()
}
)
.is_none()); .is_none());
let horz_size = tab let horz_size = tab
.compute_split_size(0, SplitDirection::Horizontal) .compute_split_size(
0,
SplitRequest {
direction: SplitDirection::Horizontal,
..Default::default()
},
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
horz_size, horz_size,
SplitDirectionAndSize { SplitDirectionAndSize {
direction: SplitDirection::Horizontal, direction: SplitDirection::Horizontal,
first: PtySize { second: PtySize {
rows: 24, rows: 24,
cols: 40, cols: 40,
pixel_width: 400, pixel_width: 400,
pixel_height: 600 pixel_height: 600
}, },
second: PtySize { first: PtySize {
rows: 24, rows: 24,
cols: 39, cols: 39,
pixel_width: 390, pixel_width: 390,
@ -1809,18 +1953,26 @@ mod test {
} }
); );
let vert_size = tab.compute_split_size(0, SplitDirection::Vertical).unwrap(); let vert_size = tab
.compute_split_size(
0,
SplitRequest {
direction: SplitDirection::Vertical,
..Default::default()
},
)
.unwrap();
assert_eq!( assert_eq!(
vert_size, vert_size,
SplitDirectionAndSize { SplitDirectionAndSize {
direction: SplitDirection::Vertical, direction: SplitDirection::Vertical,
first: PtySize { second: PtySize {
rows: 12, rows: 12,
cols: 80, cols: 80,
pixel_width: 800, pixel_width: 800,
pixel_height: 300 pixel_height: 300
}, },
second: PtySize { first: PtySize {
rows: 11, rows: 11,
cols: 80, cols: 80,
pixel_width: 800, pixel_width: 800,
@ -1832,7 +1984,10 @@ mod test {
let new_index = tab let new_index = tab
.split_and_insert( .split_and_insert(
0, 0,
SplitDirection::Horizontal, SplitRequest {
direction: SplitDirection::Horizontal,
..Default::default()
},
FakePane::new(2, horz_size.second), FakePane::new(2, horz_size.second),
) )
.unwrap(); .unwrap();
@ -1845,27 +2000,40 @@ mod test {
assert_eq!(false, panes[0].is_active); assert_eq!(false, panes[0].is_active);
assert_eq!(0, panes[0].left); assert_eq!(0, panes[0].left);
assert_eq!(0, panes[0].top); assert_eq!(0, panes[0].top);
assert_eq!(40, panes[0].width); assert_eq!(39, panes[0].width);
assert_eq!(24, panes[0].height); assert_eq!(24, panes[0].height);
assert_eq!(400, panes[0].pixel_width); assert_eq!(390, panes[0].pixel_width);
assert_eq!(600, panes[0].pixel_height); assert_eq!(600, panes[0].pixel_height);
assert_eq!(1, panes[0].pane.pane_id()); assert_eq!(1, panes[0].pane.pane_id());
assert_eq!(1, panes[1].index); assert_eq!(1, panes[1].index);
assert_eq!(true, panes[1].is_active); assert_eq!(true, panes[1].is_active);
assert_eq!(41, panes[1].left); assert_eq!(40, panes[1].left);
assert_eq!(0, panes[1].top); assert_eq!(0, panes[1].top);
assert_eq!(39, panes[1].width); assert_eq!(40, panes[1].width);
assert_eq!(24, panes[1].height); assert_eq!(24, panes[1].height);
assert_eq!(390, panes[1].pixel_width); assert_eq!(400, panes[1].pixel_width);
assert_eq!(600, panes[1].pixel_height); assert_eq!(600, panes[1].pixel_height);
assert_eq!(2, panes[1].pane.pane_id()); assert_eq!(2, panes[1].pane.pane_id());
let vert_size = tab.compute_split_size(0, SplitDirection::Vertical).unwrap(); let vert_size = tab
.compute_split_size(
0,
SplitRequest {
direction: SplitDirection::Vertical,
..Default::default()
},
)
.unwrap();
let new_index = tab let new_index = tab
.split_and_insert( .split_and_insert(
0, 0,
SplitDirection::Vertical, SplitRequest {
direction: SplitDirection::Vertical,
top_level: false,
target_is_second: true,
size: Default::default(),
},
FakePane::new(3, vert_size.second), FakePane::new(3, vert_size.second),
) )
.unwrap(); .unwrap();
@ -1878,47 +2046,47 @@ mod test {
assert_eq!(false, panes[0].is_active); assert_eq!(false, panes[0].is_active);
assert_eq!(0, panes[0].left); assert_eq!(0, panes[0].left);
assert_eq!(0, panes[0].top); assert_eq!(0, panes[0].top);
assert_eq!(40, panes[0].width); assert_eq!(39, panes[0].width);
assert_eq!(12, panes[0].height); assert_eq!(11, panes[0].height);
assert_eq!(400, panes[0].pixel_width); assert_eq!(390, panes[0].pixel_width);
assert_eq!(300, panes[0].pixel_height); assert_eq!(275, panes[0].pixel_height);
assert_eq!(1, panes[0].pane.pane_id()); assert_eq!(1, panes[0].pane.pane_id());
assert_eq!(1, panes[1].index); assert_eq!(1, panes[1].index);
assert_eq!(true, panes[1].is_active); assert_eq!(true, panes[1].is_active);
assert_eq!(0, panes[1].left); assert_eq!(0, panes[1].left);
assert_eq!(13, panes[1].top); assert_eq!(12, panes[1].top);
assert_eq!(40, panes[1].width); assert_eq!(39, panes[1].width);
assert_eq!(11, panes[1].height); assert_eq!(12, panes[1].height);
assert_eq!(400, panes[1].pixel_width); assert_eq!(390, panes[1].pixel_width);
assert_eq!(275, panes[1].pixel_height); assert_eq!(300, panes[1].pixel_height);
assert_eq!(3, panes[1].pane.pane_id()); assert_eq!(3, panes[1].pane.pane_id());
assert_eq!(2, panes[2].index); assert_eq!(2, panes[2].index);
assert_eq!(false, panes[2].is_active); assert_eq!(false, panes[2].is_active);
assert_eq!(41, panes[2].left); assert_eq!(40, panes[2].left);
assert_eq!(0, panes[2].top); assert_eq!(0, panes[2].top);
assert_eq!(39, panes[2].width); assert_eq!(40, panes[2].width);
assert_eq!(24, panes[2].height); assert_eq!(24, panes[2].height);
assert_eq!(390, panes[2].pixel_width); assert_eq!(400, panes[2].pixel_width);
assert_eq!(600, panes[2].pixel_height); assert_eq!(600, panes[2].pixel_height);
assert_eq!(2, panes[2].pane.pane_id()); assert_eq!(2, panes[2].pane.pane_id());
tab.resize_split_by(1, 1); tab.resize_split_by(1, 1);
let panes = tab.iter_panes(); let panes = tab.iter_panes();
assert_eq!(40, panes[0].width); assert_eq!(39, panes[0].width);
assert_eq!(13, panes[0].height); assert_eq!(12, panes[0].height);
assert_eq!(400, panes[0].pixel_width); assert_eq!(390, panes[0].pixel_width);
assert_eq!(325, panes[0].pixel_height); assert_eq!(300, panes[0].pixel_height);
assert_eq!(40, panes[1].width); assert_eq!(39, panes[1].width);
assert_eq!(10, panes[1].height); assert_eq!(11, panes[1].height);
assert_eq!(400, panes[1].pixel_width); assert_eq!(390, panes[1].pixel_width);
assert_eq!(250, panes[1].pixel_height); assert_eq!(275, panes[1].pixel_height);
assert_eq!(39, panes[2].width); assert_eq!(40, panes[2].width);
assert_eq!(24, panes[2].height); assert_eq!(24, panes[2].height);
assert_eq!(390, panes[2].pixel_width); assert_eq!(400, panes[2].pixel_width);
assert_eq!(600, panes[2].pixel_height); assert_eq!(600, panes[2].pixel_height);
} }
} }

View File

@ -8,7 +8,7 @@ use config::{SshDomain, TlsDomainClient, UnixDomain};
use mux::connui::{ConnectionUI, ConnectionUIParams}; use mux::connui::{ConnectionUI, ConnectionUIParams};
use mux::domain::{alloc_domain_id, Domain, DomainId, DomainState}; use mux::domain::{alloc_domain_id, Domain, DomainId, DomainState};
use mux::pane::{Pane, PaneId}; use mux::pane::{Pane, PaneId};
use mux::tab::{SplitDirection, Tab, TabId}; use mux::tab::{SplitRequest, Tab, TabId};
use mux::window::WindowId; use mux::window::WindowId;
use mux::{Mux, MuxNotification}; use mux::{Mux, MuxNotification};
use portable_pty::{CommandBuilder, PtySize}; use portable_pty::{CommandBuilder, PtySize};
@ -541,7 +541,7 @@ impl Domain for ClientDomain {
command_dir: Option<String>, command_dir: Option<String>,
tab_id: TabId, tab_id: TabId,
pane_id: PaneId, pane_id: PaneId,
direction: SplitDirection, split_request: SplitRequest,
) -> anyhow::Result<Rc<dyn Pane>> { ) -> anyhow::Result<Rc<dyn Pane>> {
let inner = self let inner = self
.inner() .inner()
@ -564,7 +564,7 @@ impl Domain for ClientDomain {
.split_pane(SplitPane { .split_pane(SplitPane {
domain: SpawnTabDomain::CurrentPaneDomain, domain: SpawnTabDomain::CurrentPaneDomain,
pane_id: pane.remote_pane_id, pane_id: pane.remote_pane_id,
direction, split_request,
command, command,
command_dir, command_dir,
}) })
@ -587,7 +587,7 @@ impl Domain for ClientDomain {
None => anyhow::bail!("invalid pane id {}", pane_id), None => anyhow::bail!("invalid pane id {}", pane_id),
}; };
tab.split_and_insert(pane_index, direction, Rc::clone(&pane)) tab.split_and_insert(pane_index, split_request, Rc::clone(&pane))
.ok(); .ok();
mux.add_pane(&pane)?; mux.add_pane(&pane)?;

View File

@ -22,8 +22,8 @@ use ::wezterm_term::input::{ClickPosition, MouseButton as TMB};
use ::window::*; use ::window::*;
use anyhow::{anyhow, ensure, Context}; use anyhow::{anyhow, ensure, Context};
use config::keyassignment::{ use config::keyassignment::{
ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, Pattern, QuickSelectArguments, ClipboardCopyDestination, ClipboardPasteSource, KeyAssignment, PaneDirection, Pattern,
RotationDirection, SpawnCommand, QuickSelectArguments, RotationDirection, SpawnCommand, SplitSize,
}; };
use config::{ use config::{
configuration, AudibleBell, ConfigHandle, Dimension, DimensionContext, GradientOrientation, configuration, AudibleBell, ConfigHandle, Dimension, DimensionContext, GradientOrientation,
@ -32,7 +32,10 @@ use config::{
use mlua::{FromLua, UserData, UserDataFields}; use mlua::{FromLua, UserData, UserDataFields};
use mux::pane::{CloseReason, Pane, PaneId, Pattern as MuxPattern}; use mux::pane::{CloseReason, Pane, PaneId, Pattern as MuxPattern};
use mux::renderable::RenderableDimensions; use mux::renderable::RenderableDimensions;
use mux::tab::{PositionedPane, PositionedSplit, SplitDirection, Tab, TabId}; use mux::tab::{
PositionedPane, PositionedSplit, SplitDirection, SplitRequest, SplitSize as MuxSplitSize, Tab,
TabId,
};
use mux::window::WindowId as MuxWindowId; use mux::window::WindowId as MuxWindowId;
use mux::{Mux, MuxNotification}; use mux::{Mux, MuxNotification};
use portable_pty::PtySize; use portable_pty::PtySize;
@ -2093,11 +2096,27 @@ impl TermWindow {
} }
SplitHorizontal(spawn) => { SplitHorizontal(spawn) => {
log::trace!("SplitHorizontal {:?}", spawn); log::trace!("SplitHorizontal {:?}", spawn);
self.spawn_command(spawn, SpawnWhere::SplitPane(SplitDirection::Horizontal)); self.spawn_command(
spawn,
SpawnWhere::SplitPane(SplitRequest {
direction: SplitDirection::Horizontal,
target_is_second: true,
size: MuxSplitSize::Percent(50),
top_level: false,
}),
);
} }
SplitVertical(spawn) => { SplitVertical(spawn) => {
log::trace!("SplitVertical {:?}", spawn); log::trace!("SplitVertical {:?}", spawn);
self.spawn_command(spawn, SpawnWhere::SplitPane(SplitDirection::Vertical)); self.spawn_command(
spawn,
SpawnWhere::SplitPane(SplitRequest {
direction: SplitDirection::Vertical,
target_is_second: true,
size: MuxSplitSize::Percent(50),
top_level: false,
}),
);
} }
ToggleFullScreen => { ToggleFullScreen => {
self.window.as_ref().unwrap().toggle_fullscreen(); self.window.as_ref().unwrap().toggle_fullscreen();
@ -2488,6 +2507,37 @@ impl TermWindow {
RotationDirection::CounterClockwise => tab.rotate_counter_clockwise(), RotationDirection::CounterClockwise => tab.rotate_counter_clockwise(),
} }
} }
SplitPane(split) => {
log::trace!("SplitPane {:?}", split);
self.spawn_command(
&split.command,
SpawnWhere::SplitPane(SplitRequest {
direction: match split.direction {
PaneDirection::Down | PaneDirection::Up => SplitDirection::Vertical,
PaneDirection::Left | PaneDirection::Right => {
SplitDirection::Horizontal
}
PaneDirection::Next | PaneDirection::Prev => {
log::error!(
"Invalid direction {:?} for SplitPane",
split.direction
);
return Ok(());
}
},
target_is_second: match split.direction {
PaneDirection::Down | PaneDirection::Right => true,
PaneDirection::Up | PaneDirection::Left => false,
PaneDirection::Next | PaneDirection::Prev => unreachable!(),
},
size: match split.size {
SplitSize::Percent(n) => MuxSplitSize::Percent(n),
SplitSize::Cells(n) => MuxSplitSize::Cells(n),
},
top_level: split.top_level,
}),
);
}
}; };
Ok(()) Ok(())
} }

View File

@ -3,7 +3,7 @@ use anyhow::{anyhow, bail, Context};
use config::keyassignment::{SpawnCommand, SpawnTabDomain}; use config::keyassignment::{SpawnCommand, SpawnTabDomain};
use config::TermConfig; use config::TermConfig;
use mux::activity::Activity; use mux::activity::Activity;
use mux::tab::SplitDirection; use mux::tab::SplitRequest;
use mux::Mux; use mux::Mux;
use portable_pty::{CommandBuilder, PtySize}; use portable_pty::{CommandBuilder, PtySize};
use std::sync::Arc; use std::sync::Arc;
@ -12,7 +12,7 @@ use std::sync::Arc;
pub enum SpawnWhere { pub enum SpawnWhere {
NewWindow, NewWindow,
NewTab, NewTab,
SplitPane(SplitDirection), SplitPane(SplitRequest),
} }
impl super::TermWindow { impl super::TermWindow {

View File

@ -722,7 +722,7 @@ async fn split_pane(split: SplitPane, client_id: Option<Arc<ClientId>>) -> anyho
let (pane, size) = mux let (pane, size) = mux
.split_pane( .split_pane(
split.pane_id, split.pane_id,
split.direction, split.split_request,
split.command, split.command,
split.command_dir, split.command_dir,
split.domain, split.domain,

View File

@ -4,7 +4,7 @@ use config::keyassignment::SpawnTabDomain;
use config::wezterm_version; use config::wezterm_version;
use mux::activity::Activity; use mux::activity::Activity;
use mux::pane::PaneId; use mux::pane::PaneId;
use mux::tab::SplitDirection; use mux::tab::{SplitDirection, SplitRequest, SplitSize};
use mux::window::WindowId; use mux::window::WindowId;
use mux::Mux; use mux::Mux;
use portable_pty::cmdbuilder::CommandBuilder; use portable_pty::cmdbuilder::CommandBuilder;
@ -160,6 +160,7 @@ enum CliSubCommand {
#[structopt( #[structopt(
name = "split-pane", name = "split-pane",
rename_all = "kebab",
about = "split the current pane. about = "split the current pane.
Outputs the pane-id for the newly created pane on success" Outputs the pane-id for the newly created pane on success"
)] )]
@ -167,16 +168,47 @@ Outputs the pane-id for the newly created pane on success"
/// Specify the pane that should be split. /// Specify the pane that should be split.
/// The default is to use the current pane based on the /// The default is to use the current pane based on the
/// environment variable WEZTERM_PANE. /// environment variable WEZTERM_PANE.
#[structopt(long = "pane-id")] #[structopt(long)]
pane_id: Option<PaneId>, pane_id: Option<PaneId>,
/// Split horizontally rather than vertically /// Split horizontally rather than vertically
#[structopt(long = "horizontal")] #[structopt(long, conflicts_with_all=&["left", "right", "top", "bottom"])]
horizontal: bool, horizontal: bool,
/// Split horizontally, with the new pane on the left
#[structopt(long, conflicts_with_all=&["right", "top", "bottom"])]
left: bool,
/// Split horizontally, with the new pane on the right
#[structopt(long, conflicts_with_all=&["left", "top", "bottom"])]
right: bool,
/// Split vertically, with the new pane on the top
#[structopt(long, conflicts_with_all=&["left", "right", "bottom"])]
top: bool,
/// Split vertically, with the new pane on the bottom
#[structopt(long, conflicts_with_all=&["left", "right", "top"])]
bottom: bool,
/// Rather than splitting the active pane, split the entire
/// window.
#[structopt(long)]
top_level: bool,
/// The number of cells that the new split should have.
/// If omitted, 50% of the available space is used.
#[structopt(long)]
cells: Option<usize>,
/// Specify the number of cells that the new split should
/// have, expressed as a percentage of the available space.
#[structopt(long, conflicts_with = "cells")]
percent: Option<u8>,
/// Specify the current working directory for the initially /// Specify the current working directory for the initially
/// spawned program /// spawned program
#[structopt(long = "cwd", parse(from_os_str))] #[structopt(long, parse(from_os_str))]
cwd: Option<OsString>, cwd: Option<OsString>,
/// Instead of executing your shell, run PROG. /// Instead of executing your shell, run PROG.
@ -743,17 +775,41 @@ async fn run_cli_async(config: config::ConfigHandle, cli: CliCommand) -> anyhow:
cwd, cwd,
prog, prog,
horizontal, horizontal,
left,
right,
top,
bottom,
top_level,
cells,
percent,
} => { } => {
let pane_id = resolve_pane_id(&client, pane_id).await?; let pane_id = resolve_pane_id(&client, pane_id).await?;
let direction = if left || right || horizontal {
SplitDirection::Horizontal
} else if top || bottom {
SplitDirection::Vertical
} else {
anyhow::bail!("impossible combination of args");
};
let target_is_second = right || bottom;
let size = match (cells, percent) {
(Some(c), _) => SplitSize::Cells(c),
(_, Some(p)) => SplitSize::Percent(p),
(None, None) => SplitSize::Percent(50),
};
let split_request = SplitRequest {
direction,
target_is_second,
size,
top_level,
};
let spawned = client let spawned = client
.split_pane(codec::SplitPane { .split_pane(codec::SplitPane {
pane_id, pane_id,
direction: if horizontal { split_request,
SplitDirection::Horizontal
} else {
SplitDirection::Vertical
},
domain: config::keyassignment::SpawnTabDomain::CurrentPaneDomain, domain: config::keyassignment::SpawnTabDomain::CurrentPaneDomain,
command: if prog.is_empty() { command: if prog.is_empty() {
None None