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::{self, Pushdown as _};
use crate::internal::options;
use crate::pane::Pane;
use crate::query::Filterable;
use crate::session::Session;
use crate::snapshot::{WindowFields, WindowInfo, WindowProjection};
use crate::target::{ServerIdentity, SessionId, WindowId};
use crate::{Command, Error, ObjectKind, OptionValue};
#[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 is_zoomed(&self) -> bool {
*self.projection.window().window_zoomed_flag()
}
pub(crate) fn server_identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
pub async fn panes(&self) -> Vec<Pane> {
self.try_panes().await.unwrap_or_default()
}
pub async fn try_panes(&self) -> Result<Vec<Pane>, Error> {
let target = self.id().to_string();
let projections = listing::panes(&self.core, listing::Scope::Target(&target), None).await?;
Ok(projections
.into_iter()
.map(|projection| Pane::new(Arc::clone(&self.core), projection))
.collect())
}
pub async fn active_pane(&self) -> Result<Option<Pane>, Error> {
Ok(self.try_panes().await?.into_iter().find(Pane::is_active))
}
pub async fn session(&self) -> Result<Option<Session>, Error> {
let infos = listing::sessions(&self.core, None).await?;
Ok(infos
.into_iter()
.find(|info| info.session_id() == self.session_id())
.map(|info| Session::new(Arc::clone(&self.core), info)))
}
pub async fn refresh(&mut self) -> Result<&mut Self, Error> {
let session = self.session_id().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(name.into()),
)
.await?;
self.refresh().await?;
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(format!(
"{}:{}",
self.session_id(),
self.index()
)),
)
.await?;
self.refresh().await?;
Ok(self)
}
pub async fn select_layout(&mut self, layout: impl Into<OsString>) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"select-layout",
Command::new("select-layout")
.arg("-t")
.arg(self.id().to_string())
.arg(layout.into()),
)
.await?;
self.refresh().await?;
Ok(self)
}
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> {
listing::mutate(
&self.core,
"unlink-window",
Command::new("unlink-window").arg("-t").arg(format!(
"{}:{}",
self.session_id(),
self.index()
)),
)
.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::Window(&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::Window(&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::Window(&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::Window(&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::Window(&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::Window(&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::Window(&target), name).await
}
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 created = self.split(options).await?;
let outcome = operation(&created).await;
match (outcome, created.kill().await) {
(outcome, Ok(())) => outcome,
(Ok(_), Err(error)) => Err(error.into()),
(Err(outcome), Err(cleanup)) => {
listing::trace_discarded_cleanup(&cleanup);
Err(outcome)
}
}
}
pub async fn swap_with(&mut self, other: &Self) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"swap-window",
Command::new("swap-window")
.arg("-s")
.arg(self.id().to_string())
.arg("-t")
.arg(format!("{}:{}", other.session_id(), other.index())),
)
.await?;
self.refresh().await?;
Ok(self)
}
pub async fn move_to(&mut self, session: &Session, index: i32) -> Result<&mut Self, Error> {
listing::mutate(
&self.core,
"move-window",
Command::new("move-window")
.arg("-s")
.arg(self.id().to_string())
.arg("-t")
.arg(format!("{}:{index}", session.id())),
)
.await?;
self.refresh().await?;
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?;
Ok(self)
}
pub async fn link_to(&self, session: &Session, index: Option<i32>) -> Result<(), Error> {
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.id().to_string())
.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?;
Ok(self)
}
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::Window(&target), name)
.await?
.map(|value| OptionValue::decode(name, value)),
)
}
pub async fn pane_at(&self, index: u32) -> Result<Option<Pane>, Error> {
let target = self.id().to_string();
let projections = listing::panes(
&self.core,
listing::Scope::Target(&target),
Some(&index.predicate("pane_index")),
)
.await?;
Ok(projections
.into_iter()
.next()
.map(|projection| Pane::new(Arc::clone(&self.core), projection)))
}
}
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()
}
}
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)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SplitDirection {
Above,
Below,
Left,
Right,
}
impl SplitDirection {
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, 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, Debug)]
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 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").arg(assignment(&name, &value));
}
if let Some(shell_command) = self.command {
command = command.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.index())
}
}