1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-25 22:33:52 +03:00
wezterm/wezterm-mux-server-impl/src/sessionhandler.rs
Antoine Büsch 36f47ec3f6
Expose activate-pane-direction cli command (#2526)
* Expose `activate-pane-direction` cli command

Add a new subcommand for `wezterm cli` called `activate-pane-direction`.
It allows switching the active pane in the current tab in the given
direction.

* Bump codec version

* Replace boolean flags with a single direction arg

* Run cargo fmt
2022-09-20 08:37:05 -07:00

842 lines
31 KiB
Rust

use crate::PKI;
use anyhow::{anyhow, Context};
use codec::*;
use mux::client::ClientId;
use mux::domain::SplitSource;
use mux::pane::{Pane, PaneId};
use mux::renderable::{RenderableDimensions, StableCursorPosition};
use mux::tab::TabId;
use mux::Mux;
use promise::spawn::spawn_into_main_thread;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use termwiz::surface::SequenceNo;
use url::Url;
use wezterm_term::terminal::Alert;
use wezterm_term::StableRowIndex;
#[derive(Clone)]
pub struct PduSender {
func: Arc<dyn Fn(DecodedPdu) -> anyhow::Result<()> + Send + Sync>,
}
impl PduSender {
pub fn send(&self, pdu: DecodedPdu) -> anyhow::Result<()> {
(self.func)(pdu)
}
pub fn new<T>(f: T) -> Self
where
T: Fn(DecodedPdu) -> anyhow::Result<()> + Send + Sync + 'static,
{
Self { func: Arc::new(f) }
}
}
#[derive(Default, Debug)]
pub(crate) struct PerPane {
cursor_position: StableCursorPosition,
title: String,
working_dir: Option<Url>,
dimensions: RenderableDimensions,
mouse_grabbed: bool,
sent_initial_palette: bool,
seqno: SequenceNo,
config_generation: usize,
pub(crate) notifications: Vec<Alert>,
}
impl PerPane {
fn compute_changes(
&mut self,
pane: &Rc<dyn Pane>,
force_with_input_serial: Option<InputSerial>,
) -> Option<GetPaneRenderChangesResponse> {
let mut changed = false;
let mouse_grabbed = pane.is_mouse_grabbed();
if mouse_grabbed != self.mouse_grabbed {
changed = true;
}
let dims = pane.get_dimensions();
if dims != self.dimensions {
changed = true;
}
let cursor_position = pane.get_cursor_position();
if cursor_position != self.cursor_position {
changed = true;
}
let title = pane.get_title();
if title != self.title {
changed = true;
}
let working_dir = pane.get_current_working_dir();
if working_dir != self.working_dir {
changed = true;
}
let mut all_dirty_lines = pane.get_changed_since(
0..dims.physical_top + dims.viewport_rows as StableRowIndex,
self.seqno,
);
if !all_dirty_lines.is_empty() {
changed = true;
}
if !changed && !force_with_input_serial.is_some() {
return None;
}
// Figure out what we're going to send as dirty lines vs bonus lines
let viewport_range =
dims.physical_top..dims.physical_top + dims.viewport_rows as StableRowIndex;
let (first_line, lines) = pane.get_lines(viewport_range);
let mut bonus_lines = lines
.into_iter()
.enumerate()
.map(|(idx, mut line)| {
let stable_row = first_line + idx as StableRowIndex;
all_dirty_lines.remove(stable_row);
line.compress_for_scrollback();
(stable_row, line)
})
.collect::<Vec<_>>();
// Always send the cursor's row, as that tends to the busiest and we don't
// have a sequencing concept for our idea of the remote state.
let (cursor_line_idx, mut lines) = pane.get_lines(cursor_position.y..cursor_position.y + 1);
let mut cursor_line = lines.remove(0);
cursor_line.compress_for_scrollback();
bonus_lines.push((cursor_line_idx, cursor_line));
self.cursor_position = cursor_position;
self.title = title.clone();
self.working_dir = working_dir.clone();
self.dimensions = dims;
self.mouse_grabbed = mouse_grabbed;
self.seqno = pane.get_current_seqno();
let bonus_lines = bonus_lines.into();
Some(GetPaneRenderChangesResponse {
pane_id: pane.pane_id(),
mouse_grabbed,
dirty_lines: all_dirty_lines.iter().cloned().collect(),
dimensions: dims,
cursor_position,
title,
bonus_lines,
working_dir: working_dir.map(Into::into),
input_serial: force_with_input_serial,
seqno: self.seqno,
})
}
}
fn maybe_push_pane_changes(
pane: &Rc<dyn Pane>,
sender: PduSender,
per_pane: Arc<Mutex<PerPane>>,
) -> anyhow::Result<()> {
let mut per_pane = per_pane.lock().unwrap();
if let Some(resp) = per_pane.compute_changes(pane, None) {
sender.send(DecodedPdu {
pdu: Pdu::GetPaneRenderChangesResponse(resp),
serial: 0,
})?;
}
let config = config::configuration();
if per_pane.config_generation != config.generation() {
per_pane.config_generation = config.generation();
// If the config changed, it may have changed colors
// in the palette that we need to push down, so we
// synthesize a palette change notification to let
// the client know
per_pane.notifications.push(Alert::PaletteChanged);
per_pane.sent_initial_palette = true;
}
if !per_pane.sent_initial_palette {
per_pane.notifications.push(Alert::PaletteChanged);
per_pane.sent_initial_palette = true;
}
for alert in per_pane.notifications.drain(..) {
match alert {
Alert::PaletteChanged => {
sender.send(DecodedPdu {
pdu: Pdu::SetPalette(SetPalette {
pane_id: pane.pane_id(),
palette: pane.palette(),
}),
serial: 0,
})?;
}
alert => {
sender.send(DecodedPdu {
pdu: Pdu::NotifyAlert(NotifyAlert {
pane_id: pane.pane_id(),
alert,
}),
serial: 0,
})?;
}
}
}
Ok(())
}
pub struct SessionHandler {
to_write_tx: PduSender,
per_pane: HashMap<TabId, Arc<Mutex<PerPane>>>,
client_id: Option<Arc<ClientId>>,
}
impl Drop for SessionHandler {
fn drop(&mut self) {
if let Some(client_id) = self.client_id.take() {
let mux = Mux::get().unwrap();
mux.unregister_client(&client_id);
}
}
}
impl SessionHandler {
pub fn new(to_write_tx: PduSender) -> Self {
Self {
to_write_tx,
per_pane: HashMap::new(),
client_id: None,
}
}
pub(crate) fn per_pane(&mut self, pane_id: PaneId) -> Arc<Mutex<PerPane>> {
Arc::clone(
self.per_pane
.entry(pane_id)
.or_insert_with(|| Arc::new(Mutex::new(PerPane::default()))),
)
}
pub fn schedule_pane_push(&mut self, pane_id: PaneId) {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
maybe_push_pane_changes(&pane, sender, per_pane)?;
Ok::<(), anyhow::Error>(())
})
.detach();
}
pub fn process_one(&mut self, decoded: DecodedPdu) {
let start = Instant::now();
let sender = self.to_write_tx.clone();
let serial = decoded.serial;
if let Some(client_id) = &self.client_id {
Mux::get().unwrap().client_had_input(client_id);
}
let send_response = move |result: anyhow::Result<Pdu>| {
let pdu = match result {
Ok(pdu) => pdu,
Err(err) => Pdu::ErrorResponse(ErrorResponse {
reason: format!("Error: {}", err),
}),
};
log::trace!("{} processing time {:?}", serial, start.elapsed());
sender.send(DecodedPdu { pdu, serial }).ok();
};
fn catch<F, SND>(f: F, send_response: SND)
where
F: FnOnce() -> anyhow::Result<Pdu>,
SND: Fn(anyhow::Result<Pdu>),
{
send_response(f());
}
match decoded.pdu {
Pdu::Ping(Ping {}) => send_response(Ok(Pdu::Pong(Pong {}))),
Pdu::SetWindowWorkspace(SetWindowWorkspace {
window_id,
workspace,
}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let mut window = mux
.get_window_mut(window_id)
.ok_or_else(|| anyhow!("window {} is invalid", window_id))?;
window.set_workspace(&workspace);
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::SetClientId(SetClientId { client_id }) => {
let client_id = Arc::new(client_id);
self.client_id.replace(client_id.clone());
spawn_into_main_thread(async move {
let mux = Mux::get().unwrap();
mux.register_client(client_id);
})
.detach();
send_response(Ok(Pdu::UnitResponse(UnitResponse {})))
}
Pdu::SetFocusedPane(SetFocusedPane { pane_id }) => {
let client_id = self.client_id.clone();
spawn_into_main_thread(async move {
let mux = Mux::get().unwrap();
let _identity = mux.with_identity(client_id);
mux.record_focus_for_current_identity(pane_id);
})
.detach();
send_response(Ok(Pdu::UnitResponse(UnitResponse {})))
}
Pdu::GetClientList(GetClientList) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let clients = mux.iter_clients();
Ok(Pdu::GetClientListResponse(GetClientListResponse {
clients,
}))
},
send_response,
)
})
.detach();
}
Pdu::ListPanes(ListPanes {}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let mut tabs = vec![];
for window_id in mux.iter_windows().into_iter() {
let window = mux.get_window(window_id).unwrap();
for tab in window.iter() {
tabs.push(tab.codec_pane_tree());
}
}
log::trace!("ListPanes {:#?}", tabs);
Ok(Pdu::ListPanesResponse(ListPanesResponse { tabs }))
},
send_response,
)
})
.detach();
}
Pdu::WriteToPane(WriteToPane { pane_id, data }) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.writer().write_all(&data)?;
maybe_push_pane_changes(&pane, sender, per_pane)?;
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
);
})
.detach();
}
Pdu::KillPane(KillPane { pane_id }) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.kill();
mux.remove_pane(pane_id);
maybe_push_pane_changes(&pane, sender, per_pane)?;
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
);
})
.detach();
}
Pdu::SendPaste(SendPaste { pane_id, data }) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.send_paste(&data)?;
maybe_push_pane_changes(&pane, sender, per_pane)?;
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::SearchScrollbackRequest(SearchScrollbackRequest {
pane_id,
pattern,
range,
limit,
}) => {
use mux::pane::Pattern;
async fn do_search(
pane_id: TabId,
pattern: Pattern,
range: std::ops::Range<StableRowIndex>,
limit: Option<u32>,
) -> anyhow::Result<Pdu> {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.search(pattern, range, limit).await.map(|results| {
Pdu::SearchScrollbackResponse(SearchScrollbackResponse { results })
})
}
spawn_into_main_thread(async move {
promise::spawn::spawn(async move {
let result = do_search(pane_id, pattern, range, limit).await;
send_response(result);
})
.detach();
})
.detach();
}
Pdu::SetPaneZoomed(SetPaneZoomed {
containing_tab_id,
pane_id,
zoomed,
}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
let tab = mux
.get_tab(containing_tab_id)
.ok_or_else(|| anyhow!("no such tab {}", containing_tab_id))?;
tab.set_active_pane(&pane);
tab.set_zoomed(zoomed);
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::ActivatePaneDirection(ActivatePaneDirection { pane_id, direction }) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
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))?;
tab.activate_pane_direction(direction);
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::Resize(Resize {
containing_tab_id,
pane_id,
size,
}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.resize(size)?;
let tab = mux
.get_tab(containing_tab_id)
.ok_or_else(|| anyhow!("no such tab {}", containing_tab_id))?;
tab.rebuild_splits_sizes_from_contained_panes();
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::SendKeyDown(SendKeyDown {
pane_id,
event,
input_serial,
}) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.key_down(event.key, event.modifiers)?;
// For a key press, we want to always send back the
// cursor position so that the predictive echo doesn't
// leave the cursor in the wrong place
let mut per_pane = per_pane.lock().unwrap();
if let Some(resp) = per_pane.compute_changes(&pane, Some(input_serial))
{
sender.send(DecodedPdu {
pdu: Pdu::GetPaneRenderChangesResponse(resp),
serial: 0,
})?;
}
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::SendMouseEvent(SendMouseEvent { pane_id, event }) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
pane.mouse_event(event)?;
maybe_push_pane_changes(&pane, sender, per_pane)?;
Ok(Pdu::UnitResponse(UnitResponse {}))
},
send_response,
)
})
.detach();
}
Pdu::SpawnV2(spawn) => {
let client_id = self.client_id.clone();
spawn_into_main_thread(async move {
schedule_domain_spawn_v2(spawn, send_response, client_id);
})
.detach();
}
Pdu::SplitPane(split) => {
let client_id = self.client_id.clone();
spawn_into_main_thread(async move {
schedule_split_pane(split, send_response, client_id);
})
.detach();
}
Pdu::MovePaneToNewTab(request) => {
let client_id = self.client_id.clone();
spawn_into_main_thread(async move {
schedule_move_pane(request, send_response, client_id);
})
.detach();
}
Pdu::GetPaneRenderChanges(GetPaneRenderChanges { pane_id, .. }) => {
let sender = self.to_write_tx.clone();
let per_pane = self.per_pane(pane_id);
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let is_alive = match mux.get_pane(pane_id) {
Some(pane) => {
maybe_push_pane_changes(&pane, sender, per_pane)?;
true
}
None => false,
};
Ok(Pdu::LivenessResponse(LivenessResponse {
pane_id,
is_alive,
}))
},
send_response,
)
})
.detach();
}
Pdu::GetLines(GetLines { pane_id, lines }) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
let mut lines_and_indices = vec![];
for range in lines {
let (first_row, lines) = pane.get_lines(range);
for (idx, mut line) in lines.into_iter().enumerate() {
let stable_row = first_row + idx as StableRowIndex;
line.compress_for_scrollback();
lines_and_indices.push((stable_row, line));
}
}
Ok(Pdu::GetLinesResponse(GetLinesResponse {
pane_id,
lines: lines_and_indices.into(),
}))
},
send_response,
)
})
.detach();
}
Pdu::GetImageCell(GetImageCell {
pane_id,
line_idx,
cell_idx,
data_hash,
}) => {
spawn_into_main_thread(async move {
catch(
move || {
let mux = Mux::get().unwrap();
let mut data = None;
let pane = mux
.get_pane(pane_id)
.ok_or_else(|| anyhow!("no such pane {}", pane_id))?;
let (_, lines) = pane.get_lines(line_idx..line_idx + 1);
'found_data: for line in lines {
if let Some(cell) = line.get_cell(cell_idx) {
if let Some(images) = cell.attrs().images() {
for im in images {
if im.image_data().hash() == data_hash {
data.replace(im.image_data().clone());
break 'found_data;
}
}
}
}
}
Ok(Pdu::GetImageCellResponse(GetImageCellResponse {
pane_id,
data,
}))
},
send_response,
)
})
.detach();
}
Pdu::GetCodecVersion(_) => {
match std::env::current_exe().context("resolving current_exe") {
Err(err) => send_response(Err(err)),
Ok(executable_path) => {
send_response(Ok(Pdu::GetCodecVersionResponse(GetCodecVersionResponse {
codec_vers: CODEC_VERSION,
version_string: config::wezterm_version().to_owned(),
executable_path,
config_file_path: std::env::var_os("WEZTERM_CONFIG_FILE")
.map(Into::into),
})))
}
}
}
Pdu::GetTlsCreds(_) => {
catch(
move || {
let client_cert_pem = PKI.generate_client_cert()?;
let ca_cert_pem = PKI.ca_pem_string()?;
Ok(Pdu::GetTlsCredsResponse(GetTlsCredsResponse {
client_cert_pem,
ca_cert_pem,
}))
},
send_response,
);
}
Pdu::Invalid { .. } => send_response(Err(anyhow!("invalid PDU {:?}", decoded.pdu))),
Pdu::Pong { .. }
| Pdu::ListPanesResponse { .. }
| Pdu::SetClipboard { .. }
| Pdu::NotifyAlert { .. }
| Pdu::SetPalette { .. }
| Pdu::SpawnResponse { .. }
| Pdu::GetPaneRenderChangesResponse { .. }
| Pdu::UnitResponse { .. }
| Pdu::LivenessResponse { .. }
| Pdu::SearchScrollbackResponse { .. }
| Pdu::GetLinesResponse { .. }
| Pdu::GetCodecVersionResponse { .. }
| Pdu::WindowWorkspaceChanged { .. }
| Pdu::GetTlsCredsResponse { .. }
| Pdu::GetClientListResponse { .. }
| Pdu::PaneRemoved { .. }
| Pdu::GetImageCellResponse { .. }
| Pdu::MovePaneToNewTabResponse { .. }
| Pdu::ErrorResponse { .. } => {
send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu)))
}
}
}
}
// Dancing around a little bit here; we can't directly spawn_into_main_thread the domain_spawn
// function below because the compiler thinks that all of its locals then need to be Send.
// We need to shimmy through this helper to break that aspect of the compiler flow
// analysis and allow things to compile.
fn schedule_domain_spawn_v2<SND>(
spawn: SpawnV2,
send_response: SND,
client_id: Option<Arc<ClientId>>,
) where
SND: Fn(anyhow::Result<Pdu>) + 'static,
{
promise::spawn::spawn(async move { send_response(domain_spawn_v2(spawn, client_id).await) })
.detach();
}
fn schedule_split_pane<SND>(split: SplitPane, send_response: SND, client_id: Option<Arc<ClientId>>)
where
SND: Fn(anyhow::Result<Pdu>) + 'static,
{
promise::spawn::spawn(async move { send_response(split_pane(split, client_id).await) })
.detach();
}
async fn split_pane(split: SplitPane, client_id: Option<Arc<ClientId>>) -> anyhow::Result<Pdu> {
let mux = Mux::get().unwrap();
let _identity = mux.with_identity(client_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 source = if let Some(move_pane_id) = split.move_pane_id {
SplitSource::MovePane(move_pane_id)
} else {
SplitSource::Spawn {
command: split.command,
command_dir: split.command_dir,
}
};
let (pane, size) = mux
.split_pane(split.pane_id, split.split_request, source, split.domain)
.await?;
Ok::<Pdu, anyhow::Error>(Pdu::SpawnResponse(SpawnResponse {
pane_id: pane.pane_id(),
tab_id: tab_id,
window_id,
size,
}))
}
async fn domain_spawn_v2(spawn: SpawnV2, client_id: Option<Arc<ClientId>>) -> anyhow::Result<Pdu> {
let mux = Mux::get().unwrap();
let _identity = mux.with_identity(client_id);
let (tab, pane, window_id) = mux
.spawn_tab_or_window(
spawn.window_id,
spawn.domain,
spawn.command,
spawn.command_dir,
spawn.size,
None, // optional current pane_id
spawn.workspace,
)
.await?;
Ok::<Pdu, anyhow::Error>(Pdu::SpawnResponse(SpawnResponse {
pane_id: pane.pane_id(),
tab_id: tab.tab_id(),
window_id,
size: tab.get_size(),
}))
}
fn schedule_move_pane<SND>(
request: MovePaneToNewTab,
send_response: SND,
client_id: Option<Arc<ClientId>>,
) where
SND: Fn(anyhow::Result<Pdu>) + 'static,
{
promise::spawn::spawn(async move { send_response(move_pane(request, client_id).await) })
.detach();
}
async fn move_pane(
request: MovePaneToNewTab,
client_id: Option<Arc<ClientId>>,
) -> anyhow::Result<Pdu> {
let mux = Mux::get().unwrap();
let _identity = mux.with_identity(client_id);
let (tab, window_id) = mux
.move_pane_to_new_tab(
request.pane_id,
request.window_id,
request.workspace_for_new_window,
)
.await?;
Ok::<Pdu, anyhow::Error>(Pdu::MovePaneToNewTabResponse(MovePaneToNewTabResponse {
tab_id: tab.tab_id(),
window_id,
}))
}