mirror of
https://github.com/wez/wezterm.git
synced 2024-10-05 18:58:52 +03:00
aa81a46d58
This allows individual call sites to be able to force an immediate resolution of the process information. This should help to address https://github.com/wez/wezterm/issues/4811 wherein the problem seems to be that the cwd used for a new spawn is taken from a stale read.
490 lines
18 KiB
Rust
490 lines
18 KiB
Rust
use super::*;
|
|
use luahelper::{dynamic_to_lua_value, from_lua, to_lua};
|
|
use mlua::Value;
|
|
use mux::pane::CachePolicy;
|
|
use std::cmp::Ordering;
|
|
use std::sync::Arc;
|
|
use termwiz::cell::SemanticType;
|
|
use termwiz_funcs::lines_to_escapes;
|
|
use url_funcs::Url;
|
|
use wezterm_term::{SemanticZone, StableRowIndex};
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct MuxPane(pub PaneId);
|
|
|
|
impl MuxPane {
|
|
pub fn resolve<'a>(&self, mux: &'a Arc<Mux>) -> mlua::Result<Arc<dyn Pane>> {
|
|
mux.get_pane(self.0)
|
|
.ok_or_else(|| mlua::Error::external(format!("pane id {} not found in mux", self.0)))
|
|
}
|
|
|
|
fn get_text_from_semantic_zone(&self, zone: SemanticZone) -> mlua::Result<String> {
|
|
let mux = get_mux()?;
|
|
let pane = self.resolve(&mux)?;
|
|
|
|
let mut last_was_wrapped = false;
|
|
let first_row = zone.start_y;
|
|
let last_row = zone.end_y;
|
|
|
|
fn cols_for_row(zone: &SemanticZone, row: StableRowIndex) -> std::ops::Range<usize> {
|
|
if row < zone.start_y || row > zone.end_y {
|
|
0..0
|
|
} else if zone.start_y == zone.end_y {
|
|
// A single line zone
|
|
if zone.start_x <= zone.end_x {
|
|
zone.start_x..zone.end_x.saturating_add(1)
|
|
} else {
|
|
zone.end_x..zone.start_x.saturating_add(1)
|
|
}
|
|
} else if row == zone.end_y {
|
|
// last line of multi-line
|
|
0..zone.end_x.saturating_add(1)
|
|
} else if row == zone.start_y {
|
|
// first line of multi-line
|
|
zone.start_x..usize::max_value()
|
|
} else {
|
|
// some "middle" line of multi-line
|
|
0..usize::max_value()
|
|
}
|
|
}
|
|
|
|
let mut s = String::new();
|
|
for line in pane.get_logical_lines(zone.start_y..zone.end_y + 1) {
|
|
if !s.is_empty() && !last_was_wrapped {
|
|
s.push('\n');
|
|
}
|
|
let last_idx = line.physical_lines.len().saturating_sub(1);
|
|
for (idx, phys) in line.physical_lines.iter().enumerate() {
|
|
let this_row = line.first_row + idx as StableRowIndex;
|
|
if this_row >= first_row && this_row < last_row {
|
|
let last_phys_idx = phys.len().saturating_sub(1);
|
|
|
|
let cols = cols_for_row(&zone, this_row);
|
|
let last_col_idx = cols.end.saturating_sub(1).min(last_phys_idx);
|
|
let col_span = phys.columns_as_str(cols);
|
|
// Only trim trailing whitespace if we are the last line
|
|
// in a wrapped sequence
|
|
if idx == last_idx {
|
|
s.push_str(col_span.trim_end());
|
|
} else {
|
|
s.push_str(&col_span);
|
|
}
|
|
|
|
last_was_wrapped = last_col_idx == last_phys_idx
|
|
&& phys
|
|
.get_cell(last_col_idx)
|
|
.map(|c| c.attrs().wrapped())
|
|
.unwrap_or(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(s)
|
|
}
|
|
}
|
|
|
|
impl UserData for MuxPane {
|
|
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
methods.add_meta_method(mlua::MetaMethod::ToString, |_, this, _: ()| {
|
|
Ok(format!("MuxPane(pane_id:{}, pid:{})", this.0, unsafe {
|
|
libc::getpid()
|
|
}))
|
|
});
|
|
methods.add_method("pane_id", |_, this, _: ()| Ok(this.0));
|
|
|
|
methods.add_async_method("split", |_, this, args: Option<SplitPane>| async move {
|
|
args.unwrap_or_default().run(this).await
|
|
});
|
|
|
|
methods.add_method("send_paste", |_, this, text: String| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
pane.send_paste(&text)
|
|
.map_err(|e| mlua::Error::external(format!("{:#}", e)))?;
|
|
Ok(())
|
|
});
|
|
|
|
// An alias of send-paste for backwards compatibility with prior releases when there was a
|
|
// separate Gui-level PaneObject
|
|
methods.add_method("paste", |_, this, text: String| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
pane.send_paste(&text)
|
|
.map_err(|e| mlua::Error::external(format!("{:#}", e)))?;
|
|
Ok(())
|
|
});
|
|
|
|
methods.add_method("send_text", |_, this, text: String| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
pane.writer()
|
|
.write_all(text.as_bytes())
|
|
.map_err(|e| mlua::Error::external(format!("{:#}", e)))?;
|
|
Ok(())
|
|
});
|
|
methods.add_method("window", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
Ok(mux
|
|
.resolve_pane_id(this.0)
|
|
.map(|(_domain_id, window_id, _tab_id)| MuxWindow(window_id)))
|
|
});
|
|
methods.add_method("tab", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
Ok(mux
|
|
.resolve_pane_id(this.0)
|
|
.map(|(_domain_id, _window_id, tab_id)| MuxTab(tab_id)))
|
|
});
|
|
|
|
// For backwards compatibility with prior releases when there
|
|
// was a separate Gui-level PaneObject
|
|
methods.add_method("mux_pane", |_, this, _: ()| Ok(*this));
|
|
|
|
methods.add_method("get_title", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.get_title())
|
|
});
|
|
|
|
methods.add_method("get_current_working_dir", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane
|
|
.get_current_working_dir(CachePolicy::FetchImmediate)
|
|
.map(|url| Url { url }))
|
|
});
|
|
|
|
methods.add_method("get_metadata", |lua, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
let value = pane.get_metadata();
|
|
dynamic_to_lua_value(lua, value)
|
|
});
|
|
|
|
methods.add_method("get_foreground_process_name", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.get_foreground_process_name(CachePolicy::FetchImmediate))
|
|
});
|
|
|
|
methods.add_method("get_foreground_process_info", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.get_foreground_process_info(CachePolicy::AllowStale))
|
|
});
|
|
|
|
methods.add_method("get_cursor_position", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.get_cursor_position())
|
|
});
|
|
|
|
methods.add_method("get_dimensions", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.get_dimensions())
|
|
});
|
|
|
|
methods.add_method("get_user_vars", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.copy_user_vars())
|
|
});
|
|
|
|
methods.add_method("has_unseen_output", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.has_unseen_output())
|
|
});
|
|
|
|
methods.add_method("is_alt_screen_active", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.is_alt_screen_active())
|
|
});
|
|
|
|
// When called with no arguments, returns the lines from the
|
|
// viewport as plain text (no escape sequences).
|
|
// When called with an optional integer argument, returns the
|
|
// last nlines lines of the terminal output.
|
|
// The returned string will have trailing whitespace trimmed.
|
|
methods.add_method("get_lines_as_text", |_, this, nlines: Option<usize>| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
let dims = pane.get_dimensions();
|
|
let nlines = nlines.unwrap_or(dims.viewport_rows);
|
|
let bottom_row = dims.physical_top + dims.viewport_rows as isize;
|
|
let top_row = bottom_row.saturating_sub(nlines as isize);
|
|
let (_first_row, lines) = pane.get_lines(top_row..bottom_row);
|
|
let mut text = String::new();
|
|
for line in lines {
|
|
for cell in line.visible_cells() {
|
|
text.push_str(cell.str());
|
|
}
|
|
let trimmed = text.trim_end().len();
|
|
text.truncate(trimmed);
|
|
text.push('\n');
|
|
}
|
|
let trimmed = text.trim_end().len();
|
|
text.truncate(trimmed);
|
|
Ok(text)
|
|
});
|
|
|
|
methods.add_method("get_lines_as_escapes", |_, this, nlines: Option<usize>| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
let dims = pane.get_dimensions();
|
|
let nlines = nlines.unwrap_or(dims.viewport_rows);
|
|
let bottom_row = dims.physical_top + dims.viewport_rows as isize;
|
|
let top_row = bottom_row.saturating_sub(nlines as isize);
|
|
let (_first_row, lines) = pane.get_lines(top_row..bottom_row);
|
|
let text = lines_to_escapes(lines).map_err(mlua::Error::external)?;
|
|
Ok(text)
|
|
});
|
|
|
|
methods.add_method(
|
|
"get_logical_lines_as_text",
|
|
|_, this, nlines: Option<usize>| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
let dims = pane.get_dimensions();
|
|
let nlines = nlines.unwrap_or(dims.viewport_rows);
|
|
let bottom_row = dims.physical_top + dims.viewport_rows as isize;
|
|
let top_row = bottom_row.saturating_sub(nlines as isize);
|
|
let lines = pane.get_logical_lines(top_row..bottom_row);
|
|
let mut text = String::new();
|
|
for line in lines {
|
|
for cell in line.logical.visible_cells() {
|
|
text.push_str(cell.str());
|
|
}
|
|
let trimmed = text.trim_end().len();
|
|
text.truncate(trimmed);
|
|
text.push('\n');
|
|
}
|
|
let trimmed = text.trim_end().len();
|
|
text.truncate(trimmed);
|
|
Ok(text)
|
|
},
|
|
);
|
|
|
|
methods.add_method("get_domain_name", |_, this, _: ()| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
let mut name = None;
|
|
if let Some(mux) = Mux::try_get() {
|
|
let domain_id = pane.domain_id();
|
|
name = mux
|
|
.get_domain(domain_id)
|
|
.map(|dom| dom.domain_name().to_string());
|
|
}
|
|
match name {
|
|
Some(name) => Ok(name),
|
|
None => Ok("".to_string()),
|
|
}
|
|
});
|
|
|
|
methods.add_method("inject_output", |_, this, text: String| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
|
|
let mut parser = termwiz::escape::parser::Parser::new();
|
|
let mut actions = vec![];
|
|
parser.parse(text.as_bytes(), |action| actions.push(action));
|
|
|
|
pane.perform_actions(actions);
|
|
Ok(())
|
|
});
|
|
|
|
methods.add_method("get_semantic_zones", |lua, this, of_type: Value| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
|
|
let of_type: Option<SemanticType> = from_lua(of_type)?;
|
|
|
|
let mut zones = pane
|
|
.get_semantic_zones()
|
|
.map_err(|e| mlua::Error::external(format!("{:#}", e)))?;
|
|
|
|
if let Some(of_type) = of_type {
|
|
zones.retain(|zone| zone.semantic_type == of_type);
|
|
}
|
|
|
|
let zones = to_lua(lua, zones)?;
|
|
Ok(zones)
|
|
});
|
|
|
|
methods.add_method(
|
|
"get_semantic_zone_at",
|
|
|lua, this, (x, y): (usize, StableRowIndex)| {
|
|
let mux = get_mux()?;
|
|
let pane = this.resolve(&mux)?;
|
|
|
|
let zones = pane.get_semantic_zones().unwrap_or_else(|_| vec![]);
|
|
|
|
fn find_zone(x: usize, y: StableRowIndex, zone: &SemanticZone) -> Ordering {
|
|
match zone.start_y.cmp(&y) {
|
|
Ordering::Greater => return Ordering::Greater,
|
|
// If the zone starts on the same line then check that the
|
|
// x position is within bounds
|
|
Ordering::Equal => match zone.start_x.cmp(&x) {
|
|
Ordering::Greater => return Ordering::Greater,
|
|
Ordering::Equal | Ordering::Less => {}
|
|
},
|
|
Ordering::Less => {}
|
|
}
|
|
match zone.end_y.cmp(&y) {
|
|
Ordering::Less => Ordering::Less,
|
|
// If the zone ends on the same line then check that the
|
|
// x position is within bounds
|
|
Ordering::Equal => match zone.end_x.cmp(&x) {
|
|
Ordering::Less => Ordering::Less,
|
|
Ordering::Equal | Ordering::Greater => Ordering::Equal,
|
|
},
|
|
Ordering::Greater => Ordering::Equal,
|
|
}
|
|
}
|
|
|
|
match zones.binary_search_by(|zone| find_zone(x, y, zone)) {
|
|
Ok(idx) => {
|
|
let zone = to_lua(lua, zones[idx])?;
|
|
Ok(Some(zone))
|
|
}
|
|
Err(_) => Ok(None),
|
|
}
|
|
},
|
|
);
|
|
|
|
methods.add_method("get_text_from_semantic_zone", |_lua, this, zone: Value| {
|
|
let zone: SemanticZone = from_lua(zone)?;
|
|
this.get_text_from_semantic_zone(zone)
|
|
});
|
|
|
|
methods.add_method("get_text_from_region", |_lua, this, (start_x, start_y, end_x, end_y): (usize, StableRowIndex, usize, StableRowIndex)| {
|
|
let zone = SemanticZone {
|
|
start_x,
|
|
start_y,
|
|
end_x,
|
|
end_y,
|
|
// semantic_type is not used by get_text_from_semantic_zone
|
|
semantic_type: SemanticType::Output,
|
|
};
|
|
this.get_text_from_semantic_zone(zone)
|
|
});
|
|
|
|
methods.add_async_method("move_to_new_tab", |_lua, this, ()| async move {
|
|
let mux = Mux::get();
|
|
let (_domain, window_id, _tab) = mux
|
|
.resolve_pane_id(this.0)
|
|
.ok_or_else(|| mlua::Error::external(format!("pane {} not found", this.0)))?;
|
|
let (tab, window) = mux
|
|
.move_pane_to_new_tab(this.0, Some(window_id), None)
|
|
.await
|
|
.map_err(|e| mlua::Error::external(format!("{:#?}", e)))?;
|
|
|
|
Ok((MuxTab(tab.tab_id()), MuxWindow(window)))
|
|
});
|
|
|
|
methods.add_async_method(
|
|
"move_to_new_window",
|
|
|_lua, this, workspace: Option<String>| async move {
|
|
let mux = Mux::get();
|
|
let (tab, window) = mux
|
|
.move_pane_to_new_tab(this.0, None, workspace)
|
|
.await
|
|
.map_err(|e| mlua::Error::external(format!("{:#?}", e)))?;
|
|
|
|
Ok((MuxTab(tab.tab_id()), MuxWindow(window)))
|
|
},
|
|
);
|
|
|
|
methods.add_method("activate", move |_lua, this, ()| {
|
|
let mux = Mux::get();
|
|
let pane = this.resolve(&mux)?;
|
|
let (_domain_id, window_id, tab_id) = mux
|
|
.resolve_pane_id(this.0)
|
|
.ok_or_else(|| mlua::Error::external(format!("pane {} not found", this.0)))?;
|
|
{
|
|
let mut window = mux.get_window_mut(window_id).ok_or_else(|| {
|
|
mlua::Error::external(format!("window {window_id} not found"))
|
|
})?;
|
|
let tab_idx = window.idx_by_id(tab_id).ok_or_else(|| {
|
|
mlua::Error::external(format!(
|
|
"tab {tab_id} isn't really in window {window_id}!?"
|
|
))
|
|
})?;
|
|
window.save_and_then_set_active(tab_idx);
|
|
}
|
|
let tab = mux
|
|
.get_tab(tab_id)
|
|
.ok_or_else(|| mlua::Error::external(format!("tab {tab_id} not found")))?;
|
|
tab.set_active_pane(&pane);
|
|
Ok(())
|
|
});
|
|
|
|
methods.add_method("get_tty_name", move |_lua, this, ()| {
|
|
let mux = Mux::get();
|
|
let pane = this.resolve(&mux)?;
|
|
Ok(pane.tty_name())
|
|
});
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, FromDynamic, ToDynamic)]
|
|
struct SplitPane {
|
|
#[dynamic(flatten)]
|
|
cmd_builder: CommandBuilderFrag,
|
|
#[dynamic(default = "spawn_tab_default_domain")]
|
|
domain: SpawnTabDomain,
|
|
#[dynamic(default)]
|
|
direction: HandySplitDirection,
|
|
#[dynamic(default)]
|
|
top_level: bool,
|
|
#[dynamic(default = "default_split_size")]
|
|
size: f32,
|
|
}
|
|
impl_lua_conversion_dynamic!(SplitPane);
|
|
|
|
fn default_split_size() -> f32 {
|
|
0.5
|
|
}
|
|
|
|
impl SplitPane {
|
|
async fn run(&self, pane: &MuxPane) -> mlua::Result<MuxPane> {
|
|
let (command, command_dir) = self.cmd_builder.to_command_builder();
|
|
let source = SplitSource::Spawn {
|
|
command,
|
|
command_dir,
|
|
};
|
|
|
|
let size = if self.size == 0.0 {
|
|
SplitSize::Percent(50)
|
|
} else if self.size < 1.0 {
|
|
SplitSize::Percent((self.size * 100.).floor() as u8)
|
|
} else {
|
|
SplitSize::Cells(self.size as usize)
|
|
};
|
|
|
|
let direction = match self.direction {
|
|
HandySplitDirection::Right | HandySplitDirection::Left => SplitDirection::Horizontal,
|
|
HandySplitDirection::Top | HandySplitDirection::Bottom => SplitDirection::Vertical,
|
|
};
|
|
|
|
let request = SplitRequest {
|
|
direction,
|
|
target_is_second: match self.direction {
|
|
HandySplitDirection::Top | HandySplitDirection::Left => false,
|
|
HandySplitDirection::Bottom | HandySplitDirection::Right => true,
|
|
},
|
|
top_level: self.top_level,
|
|
size,
|
|
};
|
|
|
|
let mux = get_mux()?;
|
|
let (pane, _size) = mux
|
|
.split_pane(pane.0, request, source, self.domain.clone())
|
|
.await
|
|
.map_err(|e| mlua::Error::external(format!("{:#?}", e)))?;
|
|
|
|
Ok(MuxPane(pane.pane_id()))
|
|
}
|
|
}
|