1
1
mirror of https://github.com/wez/wezterm.git synced 2024-10-05 18:58:52 +03:00
wezterm/lua-api-crates/mux/src/pane.rs
Wez Furlong aa81a46d58
mux: allow specifying cache policy for process information
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.
2024-02-02 18:02:18 -07:00

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()))
}
}