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::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 anyhow::Result<()> + Send + Sync>, } impl PduSender { pub fn send(&self, pdu: DecodedPdu) -> anyhow::Result<()> { (self.func)(pdu) } pub fn new(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, dimensions: RenderableDimensions, mouse_grabbed: bool, sent_initial_palette: bool, seqno: SequenceNo, config_generation: usize, pub(crate) notifications: Vec, } impl PerPane { fn compute_changes( &mut self, pane: &Arc, force_with_input_serial: Option, ) -> Option { 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::>(); // 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: &Arc, sender: PduSender, per_pane: Arc>, ) -> 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>>, client_id: Option>, } 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> { 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| { 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: F, send_response: SND) where F: FnOnce() -> anyhow::Result, SND: Fn(anyhow::Result), { 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, limit: Option, ) -> anyhow::Result { 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( spawn: SpawnV2, send_response: SND, client_id: Option>, ) where SND: Fn(anyhow::Result) + 'static, { promise::spawn::spawn(async move { send_response(domain_spawn_v2(spawn, client_id).await) }) .detach(); } fn schedule_split_pane(split: SplitPane, send_response: SND, client_id: Option>) where SND: Fn(anyhow::Result) + 'static, { promise::spawn::spawn(async move { send_response(split_pane(split, client_id).await) }) .detach(); } async fn split_pane(split: SplitPane, client_id: Option>) -> anyhow::Result { 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::SpawnResponse(SpawnResponse { pane_id: pane.pane_id(), tab_id: tab_id, window_id, size, })) } async fn domain_spawn_v2(spawn: SpawnV2, client_id: Option>) -> anyhow::Result { 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::SpawnResponse(SpawnResponse { pane_id: pane.pane_id(), tab_id: tab.tab_id(), window_id, size: tab.get_size(), })) } fn schedule_move_pane( request: MovePaneToNewTab, send_response: SND, client_id: Option>, ) where SND: Fn(anyhow::Result) + 'static, { promise::spawn::spawn(async move { send_response(move_pane(request, client_id).await) }) .detach(); } async fn move_pane( request: MovePaneToNewTab, client_id: Option>, ) -> anyhow::Result { 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::MovePaneToNewTabResponse(MovePaneToNewTabResponse { tab_id: tab.tab_id(), window_id, })) }