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::scoped;
use crate::pane::Pane;
#[cfg(feature = "query")]
use crate::query::{FilterSchema, Filterable};
use crate::session::Session;
use crate::snapshot::WindowProjection;
#[cfg(feature = "query")]
use crate::snapshot::{WindowFields, WindowInfo};
use crate::target::{ServerIdentity, SessionId, WindowId};
use crate::{Command, CommandResult, Error, ObjectKind};
mod navigation;
mod settings;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum PaneDirection {
Above,
Below,
Left,
Right,
}
impl PaneDirection {
const fn flag(self) -> &'static str {
match self {
Self::Above => "-U",
Self::Below => "-D",
Self::Left => "-L",
Self::Right => "-R",
}
}
}
#[derive(Clone)]
pub struct Window {
core: Arc<Core>,
projection: WindowProjection,
}
impl Window {
pub(crate) const fn new(core: Arc<Core>, projection: WindowProjection) -> 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> {
let Some(pane) = Pane::from_env_value(server, value).await? else {
return Ok(None);
};
server.window_by_id(pane.window_id()).await
}
#[must_use]
pub const fn id(&self) -> &WindowId {
self.projection.window().window_id()
}
#[must_use]
pub const fn session_id(&self) -> &SessionId {
self.projection.link().identity().session_id()
}
#[must_use]
pub const fn index(&self) -> i32 {
self.projection.link().identity().window_index()
}
#[must_use]
pub fn name(&self) -> &TmuxText {
self.projection.window().window_name()
}
#[must_use]
pub fn pane_count(&self) -> u32 {
*self.projection.window().window_panes()
}
#[must_use]
pub fn width(&self) -> u32 {
*self.projection.window().window_width()
}
#[must_use]
pub fn height(&self) -> u32 {
*self.projection.window().window_height()
}
#[must_use]
pub fn layout(&self) -> &TmuxText {
self.projection.window().window_layout()
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.projection.link().is_active()
}
#[must_use]
pub const fn is_linked(&self) -> bool {
self.projection.link().is_linked()
}
#[must_use]
pub const fn has_activity(&self) -> bool {
self.projection.link().has_activity()
}
#[must_use]
pub const fn has_bell(&self) -> bool {
self.projection.link().has_bell()
}
#[must_use]
pub fn last_activity(&self) -> i64 {
*self.projection.window().window_activity()
}
#[must_use]
pub fn is_zoomed(&self) -> bool {
*self.projection.window().window_zoomed_flag()
}
pub(crate) fn server_identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
fn link_target(&self) -> String {
format!("{}:{}", self.session_id(), self.id())
}
pub async fn refresh(&mut self) -> Result<&mut Self, Error> {
let session = self.session_id().clone();
self.refresh_in(&session).await
}
async fn refresh_in(&mut self, session: &SessionId) -> Result<&mut Self, Error> {
let session = session.to_string();
let projection = listing::windows(&self.core, listing::Scope::Target(&session), None)
.await?
.into_iter()
.find(|projection| projection.window().window_id() == self.id())
.ok_or_else(|| Error::ObjectGone {
kind: ObjectKind::Window,
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 split(&self, options: impl Into<SplitOptions>) -> Result<Pane, Error> {
let options = options.into();
let window = self.id().to_string();
let projection =
listing::create_pane(&self.core, |format| options.into_command(&window, format))
.await?;
Ok(Pane::new(Arc::clone(&self.core), projection))
}
pub async fn rename(&mut self, name: impl Into<OsString>) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"rename-window",
Command::new("rename-window")
.arg("-t")
.arg(self.id().to_string())
.arg("--")
.arg(name.into()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("rename-window"))?;
Ok(self)
}
pub async fn select(&mut self) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"select-window",
Command::new("select-window")
.arg("-t")
.arg(self.id().to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("select-window"))?;
Ok(self)
}
pub async fn select_layout(
&mut self,
layout: impl Into<LayoutSpec>,
) -> Result<&mut Self, Error> {
let layout = layout.into();
let argument = match &layout {
LayoutSpec::Named(named) => {
crate::Server::from_core(Arc::clone(&self.core))
.require(named.as_str(), named.minimum_release())
.await?;
OsString::from(named.as_str())
}
LayoutSpec::Saved(saved) => saved.clone(),
};
listing::mutate(
&self.core,
"select-layout",
Command::new("select-layout")
.arg("-t")
.arg(self.id().to_string())
.arg("--")
.arg(argument),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("select-layout"))?;
Ok(self)
}
pub async fn respawn(
&mut self,
command: Option<impl Into<OsString>>,
kill: bool,
) -> Result<&mut Self, Error> {
let mut respawn = Command::new("respawn-window")
.arg("-t")
.arg(self.id().to_string());
if kill {
respawn = respawn.arg("-k");
}
if let Some(command) = command {
respawn = respawn.arg("--").arg(command.into());
}
listing::mutate(&self.core, "respawn-window", respawn).await?;
self.refresh()
.await
.map_err(|error| error.after_effect("respawn-window"))?;
Ok(self)
}
pub async fn next_layout(&mut self) -> Result<&mut Self, Error> {
self.step_layout("-n").await
}
pub async fn previous_layout(&mut self) -> Result<&mut Self, Error> {
self.step_layout("-p").await
}
async fn step_layout(&mut self, flag: &'static str) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"select-layout",
Command::new("select-layout")
.arg(flag)
.arg("-t")
.arg(self.id().to_string()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("select-layout"))?;
Ok(self)
}
pub async fn rotate(&self, rotation: Rotation) -> Result<(), Error> {
listing::mutate(
&self.core,
"rotate-window",
Command::new("rotate-window")
.arg(rotation.flag())
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn kill(self) -> Result<(), Error> {
listing::mutate(
&self.core,
"kill-window",
Command::new("kill-window")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn unlink(self) -> Result<(), Error> {
let Err(error) = listing::mutate(
&self.core,
"unlink-window",
Command::new("unlink-window").arg("-t").arg(format!(
"{}:{}",
self.session_id(),
self.id()
)),
)
.await
else {
return Ok(());
};
Err(link_or_object_gone(&self.core, error, &[(self.session_id(), self.id())]).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 with_pane<T, E>(
&self,
options: impl Into<SplitOptions>,
operation: impl AsyncFnOnce(&Pane) -> Result<T, E>,
) -> Result<T, E>
where
E: From<Error>,
{
let window = self.clone();
let options = options.into();
scoped::run(
"with-pane",
async move { window.split(options).await },
Pane::kill,
operation,
)
.await
}
pub async fn swap_with(&mut self, other: &Self) -> Result<&mut Self, Error> {
self.core
.require_same_server(other.server_identity(), "swap-window")?;
if let Err(error) = listing::mutate(
&self.core,
"swap-window",
Command::new("swap-window")
.arg("-s")
.arg(self.link_target())
.arg("-t")
.arg(format!("{}:{}", other.session_id(), other.id())),
)
.await
{
return Err(link_or_object_gone(
&self.core,
error,
&[
(self.session_id(), self.id()),
(other.session_id(), other.id()),
],
)
.await);
}
self.refresh_in(other.session_id())
.await
.map_err(|error| error.after_effect("swap-window"))?;
Ok(self)
}
pub async fn move_to(&mut self, session: &Session, index: i32) -> Result<&mut Self, Error> {
self.core
.require_same_server(session.server_identity(), "move-window")?;
listing::mutate(
&self.core,
"move-window",
Command::new("move-window")
.arg("-s")
.arg(self.link_target())
.arg("-t")
.arg(format!("{}:{index}", session.id())),
)
.await?;
self.refresh_in(session.id())
.await
.map_err(|error| error.after_effect("move-window"))?;
Ok(self)
}
pub async fn resize_by(
&mut self,
direction: ResizeDirection,
cells: u32,
) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"resize-window",
Command::new("resize-window")
.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-window"))?;
Ok(self)
}
pub async fn link_to(&self, session: &Session, index: Option<i32>) -> Result<(), Error> {
self.core
.require_same_server(session.server_identity(), "link-window")?;
let target = index.map_or_else(
|| session.id().to_string(),
|index| format!("{}:{index}", session.id()),
);
listing::mutate(
&self.core,
"link-window",
Command::new("link-window")
.arg("-s")
.arg(self.link_target())
.arg("-t")
.arg(target),
)
.await?;
Ok(())
}
pub async fn resize(&mut self, width: u32, height: u32) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"resize-window",
Command::new("resize-window")
.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-window"))?;
Ok(self)
}
}
impl PartialEq for Window {
fn eq(&self, other: &Self) -> bool {
self.server_identity() == other.server_identity()
&& self.projection.link().identity() == other.projection.link().identity()
}
}
impl Eq for Window {}
impl Hash for Window {
fn hash<H: Hasher>(&self, state: &mut H) {
self.server_identity().hash(state);
self.projection.link().identity().hash(state);
}
}
impl fmt::Debug for Window {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Window")
.field("id", &self.id())
.field("session_id", &self.session_id())
.field("index", &self.index())
.finish_non_exhaustive()
}
}
#[cfg(feature = "query")]
impl Filterable for Window {
type Fields = WindowFields<Self>;
const FILTER_TARGET: &'static str = <WindowInfo 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.window().__filter_matches(predicate)
}
fn __filter_validate(
predicate: &crate::query::__private::Predicate,
) -> Result<(), crate::query::FilterExpressionError> {
<WindowInfo as Filterable>::__filter_validate(predicate)
}
}
#[cfg(feature = "query")]
impl FilterSchema for Window {
fn __filter_schema() -> crate::query::__private::FilterSchemaDescriptor {
<WindowInfo as FilterSchema>::__filter_schema()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Rotation {
Up,
Down,
}
impl Rotation {
const fn flag(self) -> &'static str {
match self {
Self::Up => "-U",
Self::Down => "-D",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Layout {
EvenHorizontal,
EvenVertical,
MainHorizontal,
MainHorizontalMirrored,
MainVertical,
MainVerticalMirrored,
Tiled,
}
impl Layout {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::EvenHorizontal => "even-horizontal",
Self::EvenVertical => "even-vertical",
Self::MainHorizontal => "main-horizontal",
Self::MainHorizontalMirrored => "main-horizontal-mirrored",
Self::MainVertical => "main-vertical",
Self::MainVerticalMirrored => "main-vertical-mirrored",
Self::Tiled => "tiled",
}
}
#[must_use]
pub const fn minimum_release(self) -> crate::ReleaseVersion {
match self {
Self::MainHorizontalMirrored | Self::MainVerticalMirrored => {
crate::version::since::MIRRORED_LAYOUTS
}
_ => crate::TmuxVersion::MIN_SUPPORTED,
}
}
}
impl fmt::Display for Layout {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LayoutSpec {
Named(Layout),
Saved(OsString),
}
impl From<Layout> for LayoutSpec {
fn from(layout: Layout) -> Self {
Self::Named(layout)
}
}
impl From<OsString> for LayoutSpec {
fn from(saved: OsString) -> Self {
Self::Saved(saved)
}
}
impl From<String> for LayoutSpec {
fn from(saved: String) -> Self {
Self::Saved(saved.into())
}
}
impl From<&str> for LayoutSpec {
fn from(saved: &str) -> Self {
Self::Saved(saved.into())
}
}
impl From<&OsStr> for LayoutSpec {
fn from(saved: &OsStr) -> Self {
Self::Saved(saved.to_owned())
}
}
impl From<&TmuxText> for LayoutSpec {
fn from(saved: &TmuxText) -> Self {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt as _;
Self::Saved(OsString::from_vec(saved.as_bytes().to_vec()))
}
#[cfg(not(unix))]
{
Self::Saved(OsString::from(saved.to_string_lossy().into_owned()))
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SplitDirection {
Above,
Below,
Left,
Right,
}
impl SplitDirection {
pub(crate) const fn flags(self) -> (&'static str, bool) {
match self {
Self::Above => ("-v", true),
Self::Below => ("-v", false),
Self::Left => ("-h", true),
Self::Right => ("-h", false),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct JoinOptions {
direction: SplitDirection,
size: Option<PaneSize>,
full: bool,
}
impl JoinOptions {
#[must_use]
pub const fn new(direction: SplitDirection) -> Self {
Self {
direction,
size: None,
full: false,
}
}
#[must_use]
pub const fn size(mut self, size: PaneSize) -> Self {
self.size = Some(size);
self
}
#[must_use]
pub const fn full(mut self) -> Self {
self.full = true;
self
}
pub(crate) fn apply(self, command: Command) -> Command {
let (axis, before) = self.direction.flags();
let mut command = command.arg(axis);
if before {
command = command.arg("-b");
}
if self.full {
command = command.arg("-f");
}
if let Some(size) = self.size {
command = command.arg("-l").arg(size.to_string());
}
command
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum PaneSize {
Cells(u32),
Percent(u32),
}
impl fmt::Display for PaneSize {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cells(cells) => write!(formatter, "{cells}"),
Self::Percent(percent) => write!(formatter, "{percent}%"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ResizeDirection {
Up,
Down,
Left,
Right,
}
impl ResizeDirection {
pub(crate) const fn flag(self) -> &'static str {
match self {
Self::Up => "-U",
Self::Down => "-D",
Self::Left => "-L",
Self::Right => "-R",
}
}
}
#[must_use = "options describe a split but do not perform one"]
#[derive(Clone)]
pub struct SplitOptions {
direction: SplitDirection,
start_directory: Option<std::path::PathBuf>,
command: Option<OsString>,
size: Option<PaneSize>,
environment: Vec<(OsString, OsString)>,
full: bool,
zoom: bool,
select: bool,
}
impl fmt::Debug for SplitOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SplitOptions")
.field("direction", &self.direction)
.field("has_start_directory", &self.start_directory.is_some())
.field("has_command", &self.command.is_some())
.field("size", &self.size)
.field("environment_count", &self.environment.len())
.field("full", &self.full)
.field("zoom", &self.zoom)
.field("select", &self.select)
.finish()
}
}
impl SplitOptions {
pub const fn new(direction: SplitDirection) -> Self {
Self {
direction,
start_directory: None,
command: None,
size: None,
environment: Vec::new(),
full: false,
zoom: false,
select: false,
}
}
pub fn start_directory(mut self, directory: impl Into<std::path::PathBuf>) -> Self {
self.start_directory = Some(directory.into());
self
}
pub fn command(mut self, command: impl Into<OsString>) -> Self {
self.command = Some(command.into());
self
}
pub const fn size(mut self, size: PaneSize) -> Self {
self.size = Some(size);
self
}
pub fn environment(mut self, name: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.environment.push((name.into(), value.into()));
self
}
pub const fn full(mut self) -> Self {
self.full = true;
self
}
pub const fn zoom(mut self) -> Self {
self.zoom = true;
self
}
pub const fn select(mut self) -> Self {
self.select = true;
self
}
pub(crate) fn into_command(self, target: &str, print_format: &str) -> Command {
let (axis, before) = self.direction.flags();
let mut command = Command::new("split-window")
.arg("-P")
.arg("-F")
.arg(print_format)
.arg("-t")
.arg(target)
.arg(axis);
if before {
command = command.arg("-b");
}
if !self.select {
command = command.arg("-d");
}
if self.full {
command = command.arg("-f");
}
if self.zoom {
command = command.arg("-Z");
}
if let Some(size) = self.size {
command = command.arg("-l").arg(size.to_string());
}
if let Some(directory) = self.start_directory {
command = command.arg("-c").arg(directory.into_os_string());
}
for (name, value) in self.environment {
command = command.arg("-e").sensitive_arg(assignment(&name, &value));
}
if let Some(shell_command) = self.command {
command = command.sensitive_arg(shell_command);
}
command
}
}
impl From<SplitDirection> for SplitOptions {
fn from(direction: SplitDirection) -> Self {
Self::new(direction)
}
}
pub(crate) fn assignment(name: &OsStr, value: &OsStr) -> OsString {
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
let mut bytes = name.as_bytes().to_vec();
bytes.push(b'=');
bytes.extend_from_slice(value.as_bytes());
OsString::from_vec(bytes)
}
impl fmt::Display for Window {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.session_id(), self.id())
}
}
async fn link_or_object_gone(
core: &Arc<Core>,
error: Error,
links: &[(&SessionId, &WindowId)],
) -> Error {
let Error::ObjectGone {
kind: ObjectKind::Window,
ref id,
} = error
else {
return error;
};
let Some(&(session, window)) = links.iter().find(|(_, window)| window.to_string() == *id)
else {
return error;
};
let alive = crate::Server::from_core(Arc::clone(core))
.window_by_id(window)
.await
.is_ok_and(|found| found.is_some());
if alive {
return Error::LinkGone {
kind: ObjectKind::Window,
target: format!("{session}:{window}"),
};
}
error
}
#[cfg(test)]
mod split_option_tests {
use super::{SplitDirection, SplitOptions};
#[test]
fn split_options_redact_process_inputs() {
let secret = "sentinel-split-process";
let options = SplitOptions::new(SplitDirection::Below)
.environment("TOKEN", secret)
.command(secret);
assert!(!format!("{options:?}").contains(secret));
let summary = options.into_command("@1", "#{pane_id}").summary();
assert_eq!(summary.sensitive_argument_count(), 2);
assert!(!summary.to_string().contains(secret));
}
}