use std::ffi::OsString;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::os::unix::ffi::OsStringExt as _;
use std::sync::Arc;
use crate::formats::TmuxText;
use crate::internal::core::Core;
use crate::internal::listing;
#[cfg(feature = "query")]
use crate::query::Filterable;
#[cfg(feature = "query")]
use crate::snapshot::ClientFields;
use crate::snapshot::ClientInfo;
use crate::target::ServerIdentity;
use crate::{Command, Error, ObjectKind};
#[derive(Clone)]
pub struct Client {
core: Arc<Core>,
info: ClientInfo,
}
impl Client {
pub(crate) const fn new(core: Arc<Core>, info: ClientInfo) -> Self {
Self { core, info }
}
#[must_use]
pub const fn name(&self) -> &TmuxText {
self.info.client_name()
}
#[must_use]
pub fn tty(&self) -> &TmuxText {
self.info.client_tty()
}
#[must_use]
pub fn term_name(&self) -> &TmuxText {
self.info.client_termname()
}
#[must_use]
pub fn pid(&self) -> u32 {
*self.info.client_pid()
}
#[must_use]
pub fn width(&self) -> u32 {
*self.info.client_width()
}
#[must_use]
pub fn height(&self) -> Option<u32> {
self.info.client_height().copied().available()
}
#[must_use]
pub fn created(&self) -> i64 {
*self.info.client_created()
}
#[must_use]
pub fn is_readonly(&self) -> bool {
*self.info.client_readonly()
}
#[must_use]
pub fn is_control_mode(&self) -> bool {
*self.info.client_control_mode()
}
pub(crate) fn server_identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
pub async fn refresh(&mut self) -> Result<&mut Self, Error> {
let info = listing::clients(&self.core, None)
.await?
.into_iter()
.find(|info| info.client_name() == self.name())
.ok_or_else(|| Error::ObjectGone {
kind: ObjectKind::Client,
id: self.name().to_string_lossy().into_owned(),
})?;
self.info = info;
Ok(self)
}
pub async fn refreshed(&self) -> Result<Self, Error> {
let mut refreshed = self.clone();
refreshed.refresh().await?;
Ok(refreshed)
}
async fn attached_id(&self, format: &str) -> Result<Option<TmuxText>, Error> {
let result = self
.core
.execute(
Command::new("display-message")
.arg("-p")
.arg("-t")
.arg(OsString::from_vec(self.name().as_bytes().to_vec()))
.arg(OsString::from(format)),
)
.await?;
if !result.success() {
let stderr = result.stderr_lossy();
if stderr.trim_end() == crate::error::NO_CURRENT_CLIENT {
return Ok(None);
}
return Err(Error::refused(
"display-message",
result.exit_code(),
stderr.into_owned(),
None,
));
}
let stdout = result.stdout();
let value = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if value.is_empty() {
return Ok(None);
}
Ok(Some(TmuxText::from(value.to_vec())))
}
pub async fn attached_session(&self) -> Result<Option<crate::Session>, Error> {
let Some(id) = self.attached_id("#{session_id}").await? else {
return Ok(None);
};
let id: crate::SessionId =
id.to_string_lossy()
.parse()
.map_err(|detail| Error::UnreadableFormatValue {
format: "#{session_id}",
detail,
})?;
crate::Server::from_core(Arc::clone(&self.core))
.session_by_id(&id)
.await
}
pub async fn attached_window(&self) -> Result<Option<crate::Window>, Error> {
let Some(id) = self.attached_id("#{window_id}").await? else {
return Ok(None);
};
let id: crate::WindowId =
id.to_string_lossy()
.parse()
.map_err(|detail| Error::UnreadableFormatValue {
format: "#{window_id}",
detail,
})?;
crate::Server::from_core(Arc::clone(&self.core))
.window_by_id(&id)
.await
}
pub async fn attached_pane(&self) -> Result<Option<crate::Pane>, Error> {
let Some(id) = self.attached_id("#{pane_id}").await? else {
return Ok(None);
};
let id: crate::PaneId =
id.to_string_lossy()
.parse()
.map_err(|detail| Error::UnreadableFormatValue {
format: "#{pane_id}",
detail,
})?;
crate::Server::from_core(Arc::clone(&self.core))
.pane_by_id(&id)
.await
}
pub async fn detach(self) -> Result<(), Error> {
listing::mutate(
&self.core,
"detach-client",
Command::new("detach-client")
.arg("-t")
.arg(self.name().to_string_lossy().into_owned()),
)
.await
}
pub async fn suspend(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"suspend-client",
Command::new("suspend-client")
.arg("-t")
.arg(self.name().to_string_lossy().into_owned()),
)
.await
}
pub async fn redraw(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"refresh-client",
Command::new("refresh-client")
.arg("-t")
.arg(self.name().to_string_lossy().into_owned()),
)
.await
}
pub async fn switch_to(&self, session: &crate::Session) -> Result<(), Error> {
listing::mutate(
&self.core,
"switch-client",
Command::new("switch-client")
.arg("-c")
.arg(self.name().to_string_lossy().into_owned())
.arg("-t")
.arg(session.id().to_string()),
)
.await
}
}
impl PartialEq for Client {
fn eq(&self, other: &Self) -> bool {
self.server_identity() == other.server_identity() && self.name() == other.name()
}
}
impl Eq for Client {}
impl Hash for Client {
fn hash<H: Hasher>(&self, state: &mut H) {
self.server_identity().hash(state);
self.name().hash(state);
}
}
impl fmt::Debug for Client {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("Client").finish_non_exhaustive()
}
}
#[cfg(feature = "query")]
impl Filterable for Client {
type Fields = ClientFields<Self>;
const FILTER_TARGET: &'static str = <ClientInfo 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.info.__filter_matches(predicate)
}
fn __filter_validate(
predicate: &crate::query::__private::Predicate,
) -> Result<(), crate::query::FilterExpressionError> {
<ClientInfo as Filterable>::__filter_validate(predicate)
}
}
impl fmt::Display for Client {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.name().to_string_lossy())
}
}