use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::formats::TmuxText;
use crate::internal::core::Core;
use crate::internal::listing;
use crate::internal::options;
#[cfg(feature = "query")]
use crate::query::Filterable;
use crate::snapshot::PaneProjection;
#[cfg(feature = "query")]
use crate::snapshot::{PaneFields, PaneInfo};
use crate::target::{PaneId, ServerIdentity, SessionId, WindowId};
use crate::window::Window;
use crate::{Command, CommandResult, Error, IndexedHooks, ObjectKind, OptionValue};
#[derive(Clone)]
pub struct Pane {
core: Arc<Core>,
projection: PaneProjection,
}
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_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> {
let session = self.session_id().to_string();
Ok(
listing::windows(&self.core, listing::Scope::Target(&session), None)
.await?
.into_iter()
.find(|projection| projection.window().window_id() == self.window_id())
.map(|projection| Window::new(Arc::clone(&self.core), projection)),
)
}
#[cfg(feature = "control-mode")]
pub async fn stream_output(&self) -> Result<crate::control::PaneOutput, Error> {
let server = crate::Server::from_core(Arc::clone(&self.core));
let (sender, events) = crate::control::ControlMode::attach(&server, self.session_id())
.await?
.split();
sender.watch_only(std::slice::from_ref(self.id())).await?;
Ok(crate::control::PaneOutput::new(
self.id().clone(),
events,
sender,
))
}
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?;
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?;
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_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 capture(&self) -> Result<Vec<TmuxText>, Error> {
self.capture_with(CaptureOptions::visible()).await
}
pub async fn capture_with(&self, options: CaptureOptions) -> Result<Vec<TmuxText>, Error> {
let command = options.into_command(self.id().as_ref());
let target = command.target().map(OsStr::to_os_string);
let result = self.core.execute(command).await?;
if !result.success() {
return Err(Error::refused(
"capture-pane",
result.exit_code(),
result.stderr_lossy().into_owned(),
target.as_deref(),
));
}
let stdout = result.stdout();
let stdout = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if stdout.is_empty() {
return Ok(Vec::new());
}
Ok(stdout
.split(|byte| *byte == b'\n')
.map(|line| TmuxText::from(line.to_vec()))
.collect())
}
pub async fn capture_lines(&self, options: CaptureOptions) -> Result<Vec<CapturedLine>, Error> {
crate::Server::from_core(Arc::clone(&self.core))
.require("capture-pane -F", crate::version::since::CAPTURE_LINE_FLAGS)
.await?;
Ok(self
.capture_with(options.line_flags())
.await?
.into_iter()
.map(|row| CapturedLine::parse(&row))
.collect())
}
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?;
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?;
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 get_option(&self, name: &str) -> Result<Option<TmuxText>, Error> {
let target = self.id().to_string();
options::get(&self.core, options::Scope::Pane(&target), name).await
}
pub async fn option_names(&self) -> Result<Vec<String>, Error> {
let target = self.id().to_string();
options::names(&self.core, options::Scope::Pane(&target)).await
}
pub async fn options(&self) -> Result<BTreeMap<String, OptionValue>, Error> {
let target = self.id().to_string();
options::typed_all(&self.core, options::Scope::Pane(&target)).await
}
pub async fn set_option(&self, name: &str, value: impl Into<OsString>) -> Result<(), Error> {
let target = self.id().to_string();
options::set(
&self.core,
options::Scope::Pane(&target),
name,
value,
false,
)
.await
}
pub async fn append_option(&self, name: &str, value: impl Into<OsString>) -> Result<(), Error> {
let target = self.id().to_string();
options::set(&self.core, options::Scope::Pane(&target), name, value, true).await
}
pub async fn unset_option(&self, name: &str) -> Result<(), Error> {
let target = self.id().to_string();
options::unset(&self.core, options::Scope::Pane(&target), name).await
}
pub async fn set_hook(&self, name: &str, command: impl Into<OsString>) -> Result<(), Error> {
let target = self.id().to_string();
options::set_hook(&self.core, options::Scope::Pane(&target), name, command).await
}
pub async fn unset_hook(&self, name: &str) -> Result<(), Error> {
let target = self.id().to_string();
options::unset_hook(&self.core, options::Scope::Pane(&target), name).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::refused(
"display-message",
result.exit_code(),
result.stderr_lossy().into_owned(),
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::refused(
"display-message",
result.exit_code(),
result.stderr_lossy().into_owned(),
Some(OsStr::new(&self.id().to_string())),
))
}
pub async fn hook(&self, name: &str) -> Result<Option<IndexedHooks>, Error> {
let target = self.id().to_string();
options::hook(&self.core, options::Scope::Pane(&target), name).await
}
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?;
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?;
Ok(self)
}
pub async fn swap_with(&mut self, other: &Self) -> Result<&mut Self, Error> {
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?;
Ok(self)
}
pub async fn break_out(self) -> Result<(), Error> {
listing::mutate(
&self.core,
"break-pane",
Command::new("break-pane")
.arg("-d")
.arg("-s")
.arg(self.id().to_string()),
)
.await
}
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,
"send-keys",
Command::new("send-keys")
.arg("-t")
.arg(self.id().to_string())
.arg("-X")
.arg("cancel"),
)
.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
}
pub async fn typed_option(&self, name: &str) -> Result<Option<OptionValue>, Error> {
let target = self.id().to_string();
Ok(
options::get(&self.core, options::Scope::Pane(&target), name)
.await?
.map(|value| OptionValue::decode(name, value)),
)
}
}
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)
}
}
impl fmt::Display for Pane {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.id())
}
}
#[must_use = "options describe a capture but do not perform one"]
#[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,
}
impl CaptureOptions {
pub const fn visible() -> Self {
Self {
start: None,
end: None,
escape_sequences: false,
join_wrapped: false,
line_flags: 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
}
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
}
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");
}
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> {
value
.and_then(|value| value.to_str())
.and_then(|value| value.parse().ok())
.ok_or_else(|| {
Error::invalid_server_configuration(crate::ServerConfigurationErrorKind::NotInsideTmux)
})
}