use std::ffi::{OsStr, OsString};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::Duration;
use crate::formats::TmuxText;
use crate::internal::core::Core;
use crate::internal::listing;
#[cfg(feature = "query")]
use crate::query::{FilterSchema, Filterable};
use crate::snapshot::PaneProjection;
#[cfg(feature = "query")]
use crate::snapshot::{PaneFields, PaneInfo};
use crate::target::{PaneId, ServerIdentity, SessionId, WindowId};
use crate::version::TmuxVersion;
use crate::window::Window;
use crate::{Command, CommandResult, Error, ObjectKind};
mod observe;
mod settings;
#[derive(Clone)]
pub struct Pane {
core: Arc<Core>,
projection: PaneProjection,
}
fn send_line_command(target: &PaneId, mut text: OsString) -> Command {
text.push("\r");
Command::new("send-keys")
.arg("-t")
.arg(target.to_string())
.arg("-l")
.sensitive_arg(text)
}
impl Pane {
pub(crate) const fn new(core: Arc<Core>, projection: PaneProjection) -> Self {
Self { core, projection }
}
pub async fn from_env(server: &crate::Server) -> Result<Option<Self>, Error> {
Self::from_env_value(server, std::env::var_os("TMUX_PANE")).await
}
pub async fn from_env_value(
server: &crate::Server,
value: Option<impl AsRef<OsStr>>,
) -> Result<Option<Self>, Error> {
server
.pane_by_id(&parse_env_id(value.as_ref().map(AsRef::as_ref))?)
.await
}
#[must_use]
pub const fn id(&self) -> &PaneId {
self.projection.pane().pane_id()
}
#[must_use]
pub const fn window_id(&self) -> &WindowId {
self.projection.link_identity().window_id()
}
#[must_use]
pub const fn session_id(&self) -> &SessionId {
self.projection.link_identity().session_id()
}
#[must_use]
pub fn index(&self) -> u32 {
*self.projection.pane().pane_index()
}
#[must_use]
pub fn current_command(&self) -> Option<&TmuxText> {
self.projection.pane().pane_current_command().available()
}
#[must_use]
pub fn current_path(&self) -> Option<&TmuxText> {
self.projection.pane().pane_current_path().available()
}
#[must_use]
pub fn title(&self) -> &TmuxText {
self.projection.pane().pane_title()
}
#[must_use]
pub fn tty(&self) -> &TmuxText {
self.projection.pane().pane_tty()
}
#[must_use]
pub fn pid(&self) -> u32 {
*self.projection.pane().pane_pid()
}
#[must_use]
pub fn width(&self) -> u32 {
*self.projection.pane().pane_width()
}
#[must_use]
pub fn height(&self) -> u32 {
*self.projection.pane().pane_height()
}
#[must_use]
pub fn is_active(&self) -> bool {
*self.projection.pane().pane_active()
}
#[must_use]
pub fn is_at_top(&self) -> bool {
*self.projection.pane().pane_at_top()
}
#[must_use]
pub fn is_at_bottom(&self) -> bool {
*self.projection.pane().pane_at_bottom()
}
#[must_use]
pub fn is_at_left(&self) -> bool {
*self.projection.pane().pane_at_left()
}
#[must_use]
pub fn is_at_right(&self) -> bool {
*self.projection.pane().pane_at_right()
}
#[must_use]
pub fn is_dead(&self) -> bool {
*self.projection.pane().pane_dead()
}
#[must_use]
pub fn is_piped(&self) -> bool {
*self.projection.pane().pane_pipe()
}
#[must_use]
pub fn is_in_mode(&self) -> bool {
*self.projection.pane().pane_in_mode() > 0
}
pub(crate) fn server_identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
pub async fn refresh(&mut self) -> Result<&mut Self, Error> {
let target = self.id().to_string();
let projection = listing::panes(&self.core, listing::Scope::Target(&target), None)
.await?
.into_iter()
.find(|projection| projection.pane().pane_id() == self.id())
.ok_or_else(|| Error::ObjectGone {
kind: ObjectKind::Pane,
id: self.id().to_string(),
})?;
self.projection = projection;
Ok(self)
}
pub async fn refreshed(&self) -> Result<Self, Error> {
let mut refreshed = self.clone();
refreshed.refresh().await?;
Ok(refreshed)
}
pub async fn window(&self) -> Result<Option<Window>, Error> {
Ok(listing::window_for_pane(&self.core, self.id())
.await?
.map(|projection| Window::new(Arc::clone(&self.core), projection)))
}
pub async fn split(&self, options: impl Into<crate::SplitOptions>) -> Result<Self, Error> {
let options = options.into();
let pane = self.id().to_string();
let projection =
listing::create_pane(&self.core, |format| options.into_command(&pane, format)).await?;
Ok(Self::new(Arc::clone(&self.core), projection))
}
pub async fn resize_by(
&mut self,
direction: crate::ResizeDirection,
cells: u32,
) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"resize-pane",
Command::new("resize-pane")
.arg("-t")
.arg(self.id().to_string())
.arg(direction.flag())
.arg(cells.to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("resize-pane"))?;
Ok(self)
}
pub async fn toggle_zoom(&mut self) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"resize-pane",
Command::new("resize-pane")
.arg("-t")
.arg(self.id().to_string())
.arg("-Z"),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("resize-pane"))?;
Ok(self)
}
pub async fn send_keys(&self, keys: impl Into<OsString>) -> Result<(), Error> {
listing::mutate(
&self.core,
"send-keys",
Command::new("send-keys")
.arg("-t")
.arg(self.id().to_string())
.arg("-l")
.sensitive_arg(keys.into()),
)
.await
}
pub async fn send_line(&self, text: impl Into<OsString>) -> Result<(), Error> {
listing::mutate(
&self.core,
"send-keys",
send_line_command(self.id(), text.into()),
)
.await
}
pub async fn send_key_names<I, K>(&self, keys: I) -> Result<(), Error>
where
I: IntoIterator<Item = K>,
K: Into<OsString>,
{
let mut command = Command::new("send-keys")
.arg("-t")
.arg(self.id().to_string());
for key in keys {
command = command.arg(key.into());
}
listing::mutate(&self.core, "send-keys", command).await
}
pub async fn select(&mut self) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"select-pane",
Command::new("select-pane")
.arg("-t")
.arg(self.id().to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("select-pane"))?;
Ok(self)
}
pub async fn resize(&mut self, width: u32, height: u32) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"resize-pane",
Command::new("resize-pane")
.arg("-t")
.arg(self.id().to_string())
.arg("-x")
.arg(width.to_string())
.arg("-y")
.arg(height.to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("resize-pane"))?;
Ok(self)
}
pub async fn kill(self) -> Result<(), Error> {
listing::mutate(
&self.core,
"kill-pane",
Command::new("kill-pane")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn cmd(&self, command: Command) -> Result<CommandResult, Error> {
self.core
.execute(command.targeting(self.id().to_string()))
.await
}
pub async fn format(&self, template: &str) -> Result<TmuxText, Error> {
let result = self
.cmd(
Command::new("display-message")
.arg("-p")
.arg(OsString::from(template)),
)
.await?;
if !result.success() {
return Err(Error::from_refused_result(
"display-message",
&result,
Some(OsStr::new(&self.id().to_string())),
));
}
let stdout = result.stdout();
Ok(TmuxText::from(
stdout.strip_suffix(b"\n").unwrap_or(stdout).to_vec(),
))
}
pub async fn display(&self, message: &str) -> Result<(), Error> {
let result = self
.cmd(Command::new("display-message").arg(OsString::from(message)))
.await?;
if result.success() {
return Ok(());
}
Err(Error::from_refused_result(
"display-message",
&result,
Some(OsStr::new(&self.id().to_string())),
))
}
pub async fn set_title(&mut self, title: impl Into<OsString>) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"select-pane",
Command::new("select-pane")
.arg("-t")
.arg(self.id().to_string())
.arg("-T")
.sensitive_arg(title.into()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("select-pane"))?;
Ok(self)
}
pub async fn clear_history(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"clear-history",
Command::new("clear-history")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn paste_buffer(&self, name: Option<&str>) -> Result<(), Error> {
let mut command = Command::new("paste-buffer")
.arg("-t")
.arg(self.id().to_string());
if let Some(name) = name {
command = command.arg("-b").arg(OsString::from(name));
}
listing::mutate(&self.core, "paste-buffer", command).await
}
pub async fn pipe(&self, command: Option<impl Into<OsString>>) -> Result<(), Error> {
let mut pipe = Command::new("pipe-pane")
.arg("-t")
.arg(self.id().to_string());
if let Some(command) = command {
pipe = pipe.sensitive_arg(command.into());
}
listing::mutate(&self.core, "pipe-pane", pipe).await
}
pub async fn respawn(
&mut self,
command: Option<impl Into<OsString>>,
kill: bool,
) -> Result<&mut Self, Error> {
let mut respawn = Command::new("respawn-pane")
.arg("-t")
.arg(self.id().to_string());
if kill {
respawn = respawn.arg("-k");
}
if let Some(command) = command {
respawn = respawn.arg(command.into());
}
listing::mutate(&self.core, "respawn-pane", respawn).await?;
self.refresh()
.await
.map_err(|error| error.after_effect("respawn-pane"))?;
Ok(self)
}
pub async fn swap_with(&mut self, other: &Self) -> Result<&mut Self, Error> {
self.core
.require_same_server(other.server_identity(), "swap-pane")?;
listing::mutate(
&self.core,
"swap-pane",
Command::new("swap-pane")
.arg("-s")
.arg(self.id().to_string())
.arg("-t")
.arg(other.id().to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("swap-pane"))?;
Ok(self)
}
pub async fn break_out(self) -> Result<Self, Error> {
listing::mutate(
&self.core,
"break-pane",
Command::new("break-pane")
.arg("-d")
.arg("-s")
.arg(self.id().to_string()),
)
.await?;
self.refreshed()
.await
.map_err(|error| error.after_effect("break-pane"))
}
pub async fn join_into(
self,
beside: &Self,
options: crate::JoinOptions,
) -> Result<Self, Error> {
self.core
.require_same_server(beside.server_identity(), "join-pane")?;
let command = options.apply(
Command::new("join-pane")
.arg("-d")
.arg("-s")
.arg(self.id().to_string())
.arg("-t")
.arg(beside.id().to_string()),
);
listing::mutate(&self.core, "join-pane", command).await?;
self.refreshed()
.await
.map_err(|error| error.after_effect("join-pane"))
}
pub async fn copy_mode(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"copy-mode",
Command::new("copy-mode")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn exit_mode(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"copy-mode",
Command::new("copy-mode")
.arg("-q")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn clock_mode(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"clock-mode",
Command::new("clock-mode")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn send_prefix(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"send-prefix",
Command::new("send-prefix")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
}
impl PartialEq for Pane {
fn eq(&self, other: &Self) -> bool {
self.server_identity() == other.server_identity() && self.id() == other.id()
}
}
impl Eq for Pane {}
impl Hash for Pane {
fn hash<H: Hasher>(&self, state: &mut H) {
self.server_identity().hash(state);
self.id().hash(state);
}
}
impl fmt::Debug for Pane {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Pane")
.field("id", &self.id())
.field("window_id", &self.window_id())
.finish_non_exhaustive()
}
}
#[cfg(feature = "query")]
impl Filterable for Pane {
type Fields = PaneFields<Self>;
const FILTER_TARGET: &'static str = <PaneInfo as Filterable>::FILTER_TARGET;
fn filter_fields() -> Self::Fields {
Self::Fields::for_target(Self::FILTER_TARGET)
}
fn __filter_matches(&self, predicate: &crate::query::__private::Predicate) -> bool {
self.projection.pane().__filter_matches(predicate)
}
fn __filter_validate(
predicate: &crate::query::__private::Predicate,
) -> Result<(), crate::query::FilterExpressionError> {
<PaneInfo as Filterable>::__filter_validate(predicate)
}
}
#[cfg(feature = "query")]
impl FilterSchema for Pane {
fn __filter_schema() -> crate::query::__private::FilterSchemaDescriptor {
<PaneInfo as FilterSchema>::__filter_schema()
}
}
impl fmt::Display for Pane {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.id())
}
}
const POLL_INTERVAL: Duration = Duration::from_millis(120);
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum PaneWait {
Arrived,
Dead,
TimedOut,
}
#[must_use = "options describe a capture but do not perform one"]
#[allow(
clippy::struct_excessive_bools,
reason = "each field is one independent capture-pane flag, and tmux \
combines them freely; there is no state to factor out"
)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct CaptureOptions {
start: Option<CaptureBound>,
end: Option<CaptureBound>,
escape_sequences: bool,
join_wrapped: bool,
line_flags: bool,
trailing_spaces: bool,
trim_blank_cells: bool,
pending_escape: bool,
}
impl CaptureOptions {
pub const fn visible() -> Self {
Self {
start: None,
end: None,
escape_sequences: false,
join_wrapped: false,
line_flags: false,
trailing_spaces: false,
trim_blank_cells: false,
pending_escape: false,
}
}
pub const fn history() -> Self {
Self {
start: Some(CaptureBound::Limit),
..Self::visible()
}
}
pub const fn start(mut self, line: i32) -> Self {
self.start = Some(CaptureBound::Line(line));
self
}
pub const fn end(mut self, line: i32) -> Self {
self.end = Some(CaptureBound::Line(line));
self
}
pub const fn escape_sequences(mut self) -> Self {
self.escape_sequences = true;
self
}
#[must_use = "options describe a capture but do not perform one"]
pub const fn trailing_spaces(mut self) -> Self {
self.trailing_spaces = true;
self
}
#[must_use = "options describe a capture but do not perform one"]
pub const fn trim_blank_cells(mut self) -> Self {
self.trim_blank_cells = true;
self
}
#[must_use = "options describe a capture but do not perform one"]
pub const fn pending_escape(mut self) -> Self {
self.pending_escape = true;
self
}
const fn line_flags(mut self) -> Self {
self.line_flags = true;
self
}
pub const fn join_wrapped(mut self) -> Self {
self.join_wrapped = true;
self
}
pub(crate) fn lower(self, pane: &str, version: &TmuxVersion) -> Result<Command, Error> {
if self.trim_blank_cells {
version.require(
"capture-pane -T",
crate::version::since::CAPTURE_TRIM_BLANK_CELLS,
)?;
}
Ok(self.into_command(pane))
}
fn into_command(self, pane: &str) -> Command {
let mut command = Command::new("capture-pane").arg("-p").arg("-t").arg(pane);
if let Some(start) = self.start {
command = command.arg("-S").arg(start.to_string());
}
if let Some(end) = self.end {
command = command.arg("-E").arg(end.to_string());
}
if self.line_flags {
command = command.arg("-F");
}
if self.escape_sequences {
command = command.arg("-e");
}
if self.join_wrapped {
command = command.arg("-J");
}
if self.trailing_spaces {
command = command.arg("-N");
}
if self.trim_blank_cells {
command = command.arg("-T");
}
if self.pending_escape {
command = command.arg("-P");
}
command
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum CaptureBound {
Limit,
Line(i32),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct CapturedLine {
pub text: TmuxText,
pub starts_prompt: bool,
pub starts_output: bool,
pub wrapped: bool,
}
impl CapturedLine {
fn parse(row: &TmuxText) -> Self {
let bytes = row.as_bytes();
let (flags, text) = bytes
.iter()
.position(|byte| *byte == b' ')
.map_or((bytes, [].as_slice()), |index| {
(&bytes[..index], &bytes[index + 1..])
});
Self {
starts_prompt: flags.contains(&b'P'),
starts_output: flags.contains(&b'O'),
wrapped: flags.contains(&b'W'),
text: TmuxText::from_bytes(text),
}
}
}
impl fmt::Display for CaptureBound {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Limit => formatter.write_str("-"),
Self::Line(line) => write!(formatter, "{line}"),
}
}
}
fn parse_env_id(value: Option<&OsStr>) -> Result<PaneId, Error> {
let Some(value) = value else {
return Err(Error::invalid_server_configuration(
crate::ServerConfigurationErrorKind::NotInsideTmux,
));
};
value
.to_str()
.and_then(|value| value.parse().ok())
.ok_or_else(|| {
Error::invalid_server_configuration(
crate::ServerConfigurationErrorKind::MalformedTmuxVariable,
)
})
}
#[cfg(test)]
mod tests {
use super::{CaptureOptions, send_line_command};
use crate::TmuxVersion;
#[test]
fn a_flag_a_release_lacks_is_refused_before_it_reaches_tmux() {
let options = CaptureOptions::visible().trim_blank_cells();
let refused = TmuxVersion::parse_output(b"tmux 3.2a\n").expect("a release");
let accepted = TmuxVersion::parse_output(b"tmux 3.4\n").expect("a release");
assert!(options.lower("%1", &refused).is_err());
assert!(options.lower("%1", &accepted).is_ok());
assert!(
CaptureOptions::history()
.join_wrapped()
.trailing_spaces()
.lower("%1", &refused)
.is_ok()
);
}
#[test]
fn a_line_keeps_one_length_independent_sensitive_argument() {
let target = "%7".parse().expect("a pane id");
for secret in ["short-secret", "a-much-longer-secret-value"] {
let summary = send_line_command(&target, secret.into()).summary();
assert_eq!(summary.sensitive_argument_count(), 1);
assert!(!summary.to_string().contains(secret));
}
}
}