mirror of
https://github.com/wez/wezterm.git
synced 2024-11-23 23:21:08 +03:00
wezterm: add wezterm cli split-pane
command
This is the initial pass; the output from the command needs some thought, but it basically operates. refs: https://github.com/wez/wezterm/issues/230
This commit is contained in:
parent
23c777aadd
commit
3a47d76094
69
src/main.rs
69
src/main.rs
@ -38,6 +38,7 @@ mod update;
|
||||
use crate::frontend::activity::Activity;
|
||||
use crate::frontend::{front_end, FrontEndSelection};
|
||||
use crate::mux::domain::{Domain, LocalDomain};
|
||||
use crate::mux::tab::{PaneId, SplitDirection};
|
||||
use crate::mux::Mux;
|
||||
use crate::server::client::{unix_connect_with_retry, Client};
|
||||
use crate::server::domain::{ClientDomain, ClientDomainConfig};
|
||||
@ -164,7 +165,7 @@ struct CliCommand {
|
||||
|
||||
#[derive(Debug, StructOpt, Clone)]
|
||||
enum CliSubCommand {
|
||||
#[structopt(name = "list", about = "list windows and tabs")]
|
||||
#[structopt(name = "list", about = "list windows, tabs and panes")]
|
||||
List,
|
||||
|
||||
#[structopt(name = "proxy", about = "start rpc proxy pipe")]
|
||||
@ -172,6 +173,34 @@ enum CliSubCommand {
|
||||
|
||||
#[structopt(name = "tlscreds", about = "obtain tls credentials")]
|
||||
TlsCreds,
|
||||
|
||||
#[structopt(
|
||||
name = "split-pane",
|
||||
about = "split the current pane.
|
||||
Outputs the pane-id for the newly created pane on success"
|
||||
)]
|
||||
SplitPane {
|
||||
/// Specify the pane that should be split.
|
||||
/// The default is to use the current pane based on the
|
||||
/// environment variable WEZTERM_PANE.
|
||||
#[structopt(long = "pane-id")]
|
||||
pane_id: Option<PaneId>,
|
||||
|
||||
/// Split horizontally rather than vertically
|
||||
#[structopt(long = "horizontal")]
|
||||
horizontal: bool,
|
||||
|
||||
/// Specify the current working directory for the initially
|
||||
/// spawned program
|
||||
#[structopt(long = "cwd", parse(from_os_str))]
|
||||
cwd: Option<OsString>,
|
||||
|
||||
/// Instead of executing your shell, run PROG.
|
||||
/// For example: `wezterm start -- bash -l` will spawn bash
|
||||
/// as if it were a login shell.
|
||||
#[structopt(parse(from_os_str))]
|
||||
prog: Vec<OsString>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt, Clone)]
|
||||
@ -969,6 +998,44 @@ fn run() -> anyhow::Result<()> {
|
||||
|
||||
tabulate_output(&cols, &data, &mut std::io::stdout().lock())?;
|
||||
}
|
||||
CliSubCommand::SplitPane {
|
||||
pane_id,
|
||||
cwd,
|
||||
prog,
|
||||
horizontal,
|
||||
} => {
|
||||
let pane_id: PaneId = match pane_id {
|
||||
Some(p) => p,
|
||||
None => std::env::var("WEZTERM_PANE")
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"--pane-id was not specified and $WEZTERM_PANE
|
||||
is not set in the environment"
|
||||
)
|
||||
})?
|
||||
.parse()?,
|
||||
};
|
||||
|
||||
let spawned = block_on(client.split_pane(crate::server::codec::SplitPane {
|
||||
pane_id,
|
||||
direction: if horizontal {
|
||||
SplitDirection::Horizontal
|
||||
} else {
|
||||
SplitDirection::Vertical
|
||||
},
|
||||
domain: keyassignment::SpawnTabDomain::CurrentPaneDomain,
|
||||
command: if prog.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let builder = CommandBuilder::from_argv(prog);
|
||||
Some(builder)
|
||||
},
|
||||
command_dir: cwd.and_then(|c| c.to_str().map(|s| s.to_string())),
|
||||
}))?;
|
||||
|
||||
log::debug!("{:?}", spawned);
|
||||
println!("{}", spawned.pane_id);
|
||||
}
|
||||
CliSubCommand::Proxy => {
|
||||
// The client object we created above will have spawned
|
||||
// the server if needed, so now all we need to do is turn
|
||||
|
@ -347,7 +347,7 @@ impl Mux {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tabs.borrow().is_empty()
|
||||
self.panes.borrow().is_empty()
|
||||
}
|
||||
|
||||
pub fn iter_panes(&self) -> Vec<Rc<dyn Pane>> {
|
||||
@ -366,6 +366,21 @@ impl Mux {
|
||||
self.domains.borrow().values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn resolve_pane_id(&self, pane_id: PaneId) -> Option<(DomainId, WindowId, TabId)> {
|
||||
let mut ids = None;
|
||||
for tab in self.tabs.borrow().values() {
|
||||
for p in tab.iter_panes() {
|
||||
if p.pane.pane_id() == pane_id {
|
||||
ids = Some((tab.tab_id(), p.pane.domain_id()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (tab_id, domain_id) = ids?;
|
||||
let window_id = self.window_containing_tab(tab_id)?;
|
||||
Some((domain_id, window_id, tab_id))
|
||||
}
|
||||
|
||||
pub fn domain_was_detached(&self, domain: DomainId) {
|
||||
self.panes
|
||||
.borrow_mut()
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::config::{configuration, SshDomain, TlsDomainClient, UnixDomain};
|
||||
use crate::connui::ConnectionUI;
|
||||
use crate::mux::domain::alloc_domain_id;
|
||||
use crate::mux::domain::DomainId;
|
||||
use crate::mux::domain::{alloc_domain_id, DomainId};
|
||||
use crate::mux::tab::PaneId;
|
||||
use crate::mux::Mux;
|
||||
use crate::server::codec::*;
|
||||
use crate::server::domain::{ClientDomain, ClientDomainConfig};
|
||||
@ -88,44 +88,65 @@ macro_rules! rpc {
|
||||
};
|
||||
}
|
||||
|
||||
fn process_unilateral_inner(pane_id: PaneId, local_domain_id: DomainId, decoded: DecodedPdu) {
|
||||
promise::spawn::spawn(async move {
|
||||
process_unilateral_inner_async(pane_id, local_domain_id, decoded).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
}
|
||||
|
||||
async fn process_unilateral_inner_async(
|
||||
pane_id: PaneId,
|
||||
local_domain_id: DomainId,
|
||||
decoded: DecodedPdu,
|
||||
) -> anyhow::Result<()> {
|
||||
let mux = Mux::get().unwrap();
|
||||
let client_domain = mux
|
||||
.get_domain(local_domain_id)
|
||||
.ok_or_else(|| anyhow!("no such domain {}", local_domain_id))?;
|
||||
let client_domain = client_domain
|
||||
.downcast_ref::<ClientDomain>()
|
||||
.ok_or_else(|| anyhow!("domain {} is not a ClientDomain instance", local_domain_id))?;
|
||||
|
||||
// If we get a push for a pane that we don't yet know about,
|
||||
// it means that some other client has manipulated the mux
|
||||
// topology; we need to re-sync.
|
||||
let local_pane_id = match client_domain.remote_to_local_pane_id(pane_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
client_domain.resync().await?;
|
||||
client_domain
|
||||
.remote_to_local_pane_id(pane_id)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("remote pane id {} does not have a local pane id", pane_id)
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
let pane = mux
|
||||
.get_pane(local_pane_id)
|
||||
.ok_or_else(|| anyhow!("no such pane {}", local_pane_id))?;
|
||||
let client_pane = pane.downcast_ref::<ClientPane>().ok_or_else(|| {
|
||||
log::error!(
|
||||
"received unilateral PDU for pane {} which is \
|
||||
not an instance of ClientPane: {:?}",
|
||||
local_pane_id,
|
||||
decoded.pdu
|
||||
);
|
||||
anyhow!(
|
||||
"received unilateral PDU for pane {} which is \
|
||||
not an instance of ClientPane: {:?}",
|
||||
local_pane_id,
|
||||
decoded.pdu
|
||||
)
|
||||
})?;
|
||||
client_pane.process_unilateral(decoded.pdu)
|
||||
}
|
||||
|
||||
fn process_unilateral(local_domain_id: DomainId, decoded: DecodedPdu) -> anyhow::Result<()> {
|
||||
if let Some(pane_id) = decoded.pdu.pane_id() {
|
||||
let pdu = decoded.pdu;
|
||||
promise::spawn::spawn_into_main_thread(async move {
|
||||
let mux = Mux::get().unwrap();
|
||||
let client_domain = mux
|
||||
.get_domain(local_domain_id)
|
||||
.ok_or_else(|| anyhow!("no such domain {}", local_domain_id))?;
|
||||
let client_domain = client_domain
|
||||
.downcast_ref::<ClientDomain>()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("domain {} is not a ClientDomain instance", local_domain_id)
|
||||
})?;
|
||||
|
||||
let local_pane_id =
|
||||
client_domain
|
||||
.remote_to_local_pane_id(pane_id)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("remote pane id {} does not have a local pane id", pane_id)
|
||||
})?;
|
||||
let pane = mux
|
||||
.get_pane(local_pane_id)
|
||||
.ok_or_else(|| anyhow!("no such pane {}", local_pane_id))?;
|
||||
let client_pane = pane.downcast_ref::<ClientPane>().ok_or_else(|| {
|
||||
log::error!(
|
||||
"received unilateral PDU for pane {} which is \
|
||||
not an instance of ClientPane: {:?}",
|
||||
local_pane_id,
|
||||
pdu
|
||||
);
|
||||
anyhow!(
|
||||
"received unilateral PDU for pane {} which is \
|
||||
not an instance of ClientPane: {:?}",
|
||||
local_pane_id,
|
||||
pdu
|
||||
)
|
||||
})?;
|
||||
client_pane.process_unilateral(pdu)
|
||||
process_unilateral_inner(pane_id, local_domain_id, decoded)
|
||||
});
|
||||
} else {
|
||||
bail!("don't know how to handle {:?}", decoded);
|
||||
@ -210,7 +231,11 @@ fn client_thread(
|
||||
log::trace!("decoded serial {}", decoded.serial);
|
||||
if decoded.serial == 0 {
|
||||
process_unilateral(local_domain_id, decoded)
|
||||
.context("processing unilateral PDU from server")?;
|
||||
.context("processing unilateral PDU from server")
|
||||
.map_err(|e| {
|
||||
log::error!("process_unilateral: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
} else if let Some(mut promise) = promises.remove(&decoded.serial) {
|
||||
promise.result(Ok(decoded.pdu));
|
||||
} else {
|
||||
@ -227,6 +252,7 @@ fn client_thread(
|
||||
for (_, mut promise) in promises.into_iter() {
|
||||
promise.result(Err(anyhow!("{}", reason)));
|
||||
}
|
||||
// FIXME: detach the domain here
|
||||
return Err(err).context("Error while decoding response pdu");
|
||||
}
|
||||
}
|
||||
@ -874,6 +900,7 @@ impl Client {
|
||||
rpc!(ping, Ping = (), Pong);
|
||||
rpc!(list_panes, ListPanes = (), ListPanesResponse);
|
||||
rpc!(spawn, Spawn, SpawnResponse);
|
||||
rpc!(split_pane, SplitPane, SpawnResponse);
|
||||
rpc!(write_to_pane, WriteToPane, UnitResponse);
|
||||
rpc!(send_paste, SendPaste, UnitResponse);
|
||||
rpc!(key_down, SendKeyDown, UnitResponse);
|
||||
|
@ -279,6 +279,7 @@ pdu! {
|
||||
SearchScrollbackRequest: 31,
|
||||
SearchScrollbackResponse: 32,
|
||||
SetPaneZoomed: 33,
|
||||
SplitPane: 34,
|
||||
}
|
||||
|
||||
impl Pdu {
|
||||
@ -496,12 +497,20 @@ pub struct Spawn {
|
||||
pub domain_id: DomainId,
|
||||
/// If None, create a new window for this new tab
|
||||
pub window_id: Option<WindowId>,
|
||||
pub split: Option<(TabId, PaneId, SplitDirection)>,
|
||||
pub command: Option<CommandBuilder>,
|
||||
pub command_dir: Option<String>,
|
||||
pub size: PtySize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
pub struct SplitPane {
|
||||
pub pane_id: PaneId,
|
||||
pub direction: SplitDirection,
|
||||
pub command: Option<CommandBuilder>,
|
||||
pub command_dir: Option<String>,
|
||||
pub domain: crate::keyassignment::SpawnTabDomain,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
pub struct SpawnResponse {
|
||||
pub tab_id: TabId,
|
||||
|
@ -2,12 +2,13 @@ use crate::config::{SshDomain, TlsDomainClient, UnixDomain};
|
||||
use crate::connui::ConnectionUI;
|
||||
use crate::font::FontConfiguration;
|
||||
use crate::frontend::front_end;
|
||||
use crate::keyassignment::SpawnTabDomain;
|
||||
use crate::mux::domain::{alloc_domain_id, Domain, DomainId, DomainState};
|
||||
use crate::mux::tab::{Pane, PaneId, SplitDirection, Tab, TabId};
|
||||
use crate::mux::window::WindowId;
|
||||
use crate::mux::Mux;
|
||||
use crate::server::client::Client;
|
||||
use crate::server::codec::{ListPanesResponse, Spawn};
|
||||
use crate::server::codec::{ListPanesResponse, Spawn, SplitPane};
|
||||
use crate::server::tab::ClientPane;
|
||||
use anyhow::{anyhow, bail};
|
||||
use async_trait::async_trait;
|
||||
@ -226,6 +227,14 @@ impl ClientDomain {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resync(&self) -> anyhow::Result<()> {
|
||||
if let Some(inner) = self.inner.borrow().as_ref() {
|
||||
let panes = inner.client.list_panes().await?;
|
||||
Self::process_pane_list(Arc::clone(inner), panes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_pane_list(inner: Arc<ClientInner>, panes: ListPanesResponse) -> anyhow::Result<()> {
|
||||
let mux = Mux::get().expect("to be called on main thread");
|
||||
log::debug!("ListPanes result {:#?}", panes);
|
||||
@ -372,7 +381,6 @@ impl Domain for ClientDomain {
|
||||
.spawn(Spawn {
|
||||
domain_id: inner.remote_domain_id,
|
||||
window_id: inner.local_to_remote_window(window),
|
||||
split: None,
|
||||
size,
|
||||
command,
|
||||
command_dir,
|
||||
@ -424,11 +432,10 @@ impl Domain for ClientDomain {
|
||||
|
||||
let result = inner
|
||||
.client
|
||||
.spawn(Spawn {
|
||||
domain_id: inner.remote_domain_id,
|
||||
window_id: None,
|
||||
split: Some((pane.remote_tab_id, pane.remote_pane_id, direction)),
|
||||
size: PtySize::default(),
|
||||
.split_pane(SplitPane {
|
||||
domain: SpawnTabDomain::CurrentPaneDomain,
|
||||
pane_id: pane.remote_tab_id,
|
||||
direction,
|
||||
command,
|
||||
command_dir,
|
||||
})
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::keyassignment::SpawnTabDomain;
|
||||
use crate::mux::renderable::{RenderableDimensions, StableCursorPosition};
|
||||
use crate::mux::tab::{Pane, PaneId, TabId};
|
||||
use crate::mux::Mux;
|
||||
@ -379,6 +380,13 @@ impl SessionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
Pdu::SplitPane(split) => {
|
||||
let sender = self.to_write_tx.clone();
|
||||
spawn_into_main_thread(async move {
|
||||
schedule_split_pane(split, sender, send_response);
|
||||
});
|
||||
}
|
||||
|
||||
Pdu::GetPaneRenderChanges(GetPaneRenderChanges { pane_id, .. }) => {
|
||||
let sender = self.to_write_tx.clone();
|
||||
let per_pane = self.per_pane(pane_id);
|
||||
@ -486,6 +494,16 @@ where
|
||||
promise::spawn::spawn(async move { send_response(domain_spawn(spawn, sender).await) });
|
||||
}
|
||||
|
||||
fn schedule_split_pane<SND>(
|
||||
split: SplitPane,
|
||||
sender: PollableSender<DecodedPdu>,
|
||||
send_response: SND,
|
||||
) where
|
||||
SND: Fn(anyhow::Result<Pdu>) + 'static,
|
||||
{
|
||||
promise::spawn::spawn(async move { send_response(split_pane(split, sender).await) });
|
||||
}
|
||||
|
||||
struct RemoteClipboard {
|
||||
sender: PollableSender<DecodedPdu>,
|
||||
pane_id: TabId,
|
||||
@ -508,45 +526,40 @@ impl Clipboard for RemoteClipboard {
|
||||
}
|
||||
}
|
||||
|
||||
async fn domain_spawn(spawn: Spawn, sender: PollableSender<DecodedPdu>) -> anyhow::Result<Pdu> {
|
||||
async fn split_pane(split: SplitPane, sender: PollableSender<DecodedPdu>) -> anyhow::Result<Pdu> {
|
||||
let mux = Mux::get().unwrap();
|
||||
let domain = mux
|
||||
.get_domain(spawn.domain_id)
|
||||
.ok_or_else(|| anyhow!("domain {} not found on this server", spawn.domain_id))?;
|
||||
let (pane_domain_id, window_id, tab_id) = mux
|
||||
.resolve_pane_id(split.pane_id)
|
||||
.ok_or_else(|| anyhow!("pane_id {} invalid", split.pane_id))?;
|
||||
|
||||
let (pane, tab_id, window_id, size) = if let Some((tab_id, pane_id, direction)) = spawn.split {
|
||||
let pane = domain
|
||||
.split_pane(spawn.command, spawn.command_dir, tab_id, pane_id, direction)
|
||||
.await?;
|
||||
let window_id = mux
|
||||
.window_containing_tab(tab_id)
|
||||
.ok_or_else(|| anyhow!("no window contains tab {}", tab_id))?;
|
||||
let dims = pane.renderer().get_dimensions();
|
||||
let size = PtySize {
|
||||
cols: dims.cols as u16,
|
||||
rows: dims.viewport_rows as u16,
|
||||
pixel_height: 0,
|
||||
pixel_width: 0,
|
||||
};
|
||||
(pane, tab_id, window_id, size)
|
||||
} else {
|
||||
let window_id = if let Some(window_id) = spawn.window_id {
|
||||
mux.get_window_mut(window_id)
|
||||
.ok_or_else(|| anyhow!("window_id {} not found on this server", window_id))?;
|
||||
window_id
|
||||
} else {
|
||||
mux.new_empty_window()
|
||||
};
|
||||
let domain = match split.domain {
|
||||
SpawnTabDomain::DefaultDomain => mux.default_domain(),
|
||||
SpawnTabDomain::CurrentPaneDomain => mux
|
||||
.get_domain(pane_domain_id)
|
||||
.expect("resolve_pane_id to give valid domain_id"),
|
||||
SpawnTabDomain::Domain(d) => mux
|
||||
.get_domain(d)
|
||||
.ok_or_else(|| anyhow!("domain id {} is invalid", d))?,
|
||||
SpawnTabDomain::DomainName(name) => mux
|
||||
.get_domain_by_name(&name)
|
||||
.ok_or_else(|| anyhow!("domain name {} is invalid", name))?,
|
||||
};
|
||||
|
||||
let tab = domain
|
||||
.spawn(spawn.size, spawn.command, spawn.command_dir, window_id)
|
||||
.await?;
|
||||
|
||||
let pane = tab
|
||||
.get_active_pane()
|
||||
.ok_or_else(|| anyhow!("missing active pane on tab!?"))?;
|
||||
|
||||
(pane, tab.tab_id(), window_id, tab.get_size())
|
||||
let pane = domain
|
||||
.split_pane(
|
||||
split.command,
|
||||
split.command_dir,
|
||||
tab_id,
|
||||
split.pane_id,
|
||||
split.direction,
|
||||
)
|
||||
.await?;
|
||||
let dims = pane.renderer().get_dimensions();
|
||||
let size = PtySize {
|
||||
cols: dims.cols as u16,
|
||||
rows: dims.viewport_rows as u16,
|
||||
pixel_height: 0,
|
||||
pixel_width: 0,
|
||||
};
|
||||
|
||||
let clip: Arc<dyn Clipboard> = Arc::new(RemoteClipboard {
|
||||
@ -562,3 +575,39 @@ async fn domain_spawn(spawn: Spawn, sender: PollableSender<DecodedPdu>) -> anyho
|
||||
size,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn domain_spawn(spawn: Spawn, sender: PollableSender<DecodedPdu>) -> anyhow::Result<Pdu> {
|
||||
let mux = Mux::get().unwrap();
|
||||
let domain = mux
|
||||
.get_domain(spawn.domain_id)
|
||||
.ok_or_else(|| anyhow!("domain {} not found on this server", spawn.domain_id))?;
|
||||
|
||||
let window_id = if let Some(window_id) = spawn.window_id {
|
||||
mux.get_window_mut(window_id)
|
||||
.ok_or_else(|| anyhow!("window_id {} not found on this server", window_id))?;
|
||||
window_id
|
||||
} else {
|
||||
mux.new_empty_window()
|
||||
};
|
||||
|
||||
let tab = domain
|
||||
.spawn(spawn.size, spawn.command, spawn.command_dir, window_id)
|
||||
.await?;
|
||||
|
||||
let pane = tab
|
||||
.get_active_pane()
|
||||
.ok_or_else(|| anyhow!("missing active pane on tab!?"))?;
|
||||
|
||||
let clip: Arc<dyn Clipboard> = Arc::new(RemoteClipboard {
|
||||
pane_id: pane.pane_id(),
|
||||
sender,
|
||||
});
|
||||
pane.set_clipboard(&clip);
|
||||
|
||||
Ok::<Pdu, anyhow::Error>(Pdu::SpawnResponse(SpawnResponse {
|
||||
pane_id: pane.pane_id(),
|
||||
tab_id: tab.tab_id(),
|
||||
window_id,
|
||||
size: tab.get_size(),
|
||||
}))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user