use std::ffi::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::snapshot::{SessionFields, SessionInfo};
use crate::target::{ServerIdentity, SessionId};
use crate::window::Window;
use crate::{Command, Error, ObjectKind, OptionValue};
#[derive(Clone)]
pub struct Session {
core: Arc<Core>,
info: SessionInfo,
}
impl Session {
pub(crate) const fn new(core: Arc<Core>, info: SessionInfo) -> Self {
Self { core, info }
}
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<std::ffi::OsStr>>,
) -> Result<Option<Self>, Error> {
let Some(pane) = Pane::from_env_value(server, value).await? else {
return Ok(None);
};
server.session_by_id(pane.session_id()).await
}
#[must_use]
pub const fn id(&self) -> &SessionId {
self.info.session_id()
}
#[must_use]
pub fn name(&self) -> &TmuxText {
self.info.session_name()
}
#[must_use]
pub fn path(&self) -> &TmuxText {
self.info.session_path()
}
#[must_use]
pub fn window_count(&self) -> u32 {
*self.info.session_windows()
}
#[must_use]
pub fn attached_client_count(&self) -> u32 {
*self.info.session_attached()
}
#[must_use]
pub fn is_attached(&self) -> bool {
self.attached_client_count() > 0
}
#[must_use]
pub fn created(&self) -> i64 {
*self.info.session_created()
}
#[must_use]
pub fn last_attached(&self) -> Option<i64> {
self.info.session_last_attached().copied().available()
}
#[must_use]
pub(crate) fn server_identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
pub async fn windows(&self) -> Vec<Window> {
self.try_windows().await.unwrap_or_default()
}
pub async fn try_windows(&self) -> Result<Vec<Window>, Error> {
let target = self.id().to_string();
let projections =
listing::windows(&self.core, listing::Scope::Target(&target), None).await?;
Ok(projections
.into_iter()
.map(|projection| Window::new(Arc::clone(&self.core), projection))
.collect())
}
pub async fn active_window(&self) -> Result<Option<Window>, Error> {
Ok(self
.try_windows()
.await?
.into_iter()
.find(Window::is_active))
}
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::SessionTarget(&target), None).await?;
Ok(projections
.into_iter()
.map(|projection| Pane::new(Arc::clone(&self.core), projection))
.collect())
}
pub async fn refresh(&mut self) -> Result<&mut Self, Error> {
let info = listing::sessions(&self.core, None)
.await?
.into_iter()
.find(|info| info.session_id() == self.id())
.ok_or_else(|| Error::ObjectGone {
kind: ObjectKind::Session,
id: self.id().to_string(),
})?;
self.info = info;
Ok(self)
}
pub async fn refreshed(&self) -> Result<Self, Error> {
let mut refreshed = self.clone();
refreshed.refresh().await?;
Ok(refreshed)
}
pub async fn new_window(&self, options: impl Into<NewWindowOptions>) -> Result<Window, Error> {
let options = options.into();
let session = self.id().to_string();
let projection =
listing::create_window(&self.core, |format| options.into_command(&session, format))
.await?;
Ok(Window::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-session",
Command::new("rename-session")
.arg("-t")
.arg(self.id().to_string())
.arg(name.into()),
)
.await?;
self.refresh().await?;
Ok(self)
}
pub async fn kill(self) -> Result<(), Error> {
listing::mutate(
&self.core,
"kill-session",
Command::new("kill-session")
.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::Session(&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::Session(&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::Session(&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::Session(&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::Session(&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::Session(&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::Session(&target), name).await
}
pub async fn with_window<T, E>(
&self,
options: impl Into<NewWindowOptions>,
operation: impl AsyncFnOnce(&Window) -> Result<T, E>,
) -> Result<T, E>
where
E: From<Error>,
{
let created = self.new_window(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 set_environment(
&self,
name: &str,
value: impl Into<OsString>,
) -> Result<(), Error> {
listing::mutate(
&self.core,
"set-environment",
Command::new("set-environment")
.arg("-t")
.arg(self.id().to_string())
.arg(OsString::from(name))
.sensitive_arg(value.into()),
)
.await
}
pub async fn environment(&self, name: &str) -> Result<Option<TmuxText>, Error> {
let result = self
.core
.execute(
Command::new("show-environment")
.arg("-t")
.arg(self.id().to_string())
.arg(OsString::from(name)),
)
.await?;
if !result.success() {
return Ok(None);
}
let stdout = result.stdout();
let line = stdout.strip_suffix(b"\n").unwrap_or(stdout);
let Some(position) = line.iter().position(|byte| *byte == b'=') else {
return Ok(None);
};
Ok(Some(TmuxText::from(line[position + 1..].to_vec())))
}
pub async fn unset_environment(&self, name: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"set-environment",
Command::new("set-environment")
.arg("-t")
.arg(self.id().to_string())
.arg("-u")
.arg(OsString::from(name)),
)
.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::Session(&target), name)
.await?
.map(|value| OptionValue::decode(name, value)),
)
}
pub async fn window(&self, name: impl AsRef<[u8]>) -> Result<Option<Window>, Error> {
let name = name.as_ref();
Ok(self
.try_windows()
.await?
.into_iter()
.find(|window| window.name() == name))
}
pub async fn window_at(&self, index: i32) -> Result<Option<Window>, Error> {
let target = self.id().to_string();
let projections = listing::windows(
&self.core,
listing::Scope::Target(&target),
Some(&index.predicate("window_index")),
)
.await?;
Ok(projections
.into_iter()
.next()
.map(|projection| Window::new(Arc::clone(&self.core), projection)))
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
self.server_identity() == other.server_identity() && self.id() == other.id()
}
}
impl Eq for Session {}
impl Hash for Session {
fn hash<H: Hasher>(&self, state: &mut H) {
self.server_identity().hash(state);
self.id().hash(state);
}
}
impl fmt::Debug for Session {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Session")
.field("id", &self.id())
.finish_non_exhaustive()
}
}
impl Filterable for Session {
type Fields = SessionFields<Self>;
const FILTER_TARGET: &'static str = <SessionInfo 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> {
<SessionInfo as Filterable>::__filter_validate(predicate)
}
}
#[must_use = "options describe a window but do not create one"]
#[derive(Clone, Debug)]
pub struct NewWindowOptions {
name: Option<OsString>,
start_directory: Option<std::path::PathBuf>,
command: Option<OsString>,
index: Option<i32>,
placement: Option<WindowPlacement>,
environment: Vec<(OsString, OsString)>,
replace_existing: bool,
select: bool,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WindowPlacement {
Before,
After,
}
impl NewWindowOptions {
pub const fn unnamed() -> Self {
Self {
name: None,
start_directory: None,
command: None,
index: None,
placement: None,
environment: Vec::new(),
replace_existing: false,
select: false,
}
}
pub fn new(name: impl Into<OsString>) -> Self {
Self {
name: Some(name.into()),
..Self::unnamed()
}
}
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 index(mut self, index: i32) -> Self {
self.index = Some(index);
self
}
pub const fn placement(mut self, placement: WindowPlacement) -> Self {
self.placement = Some(placement);
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 replace_existing(mut self) -> Self {
self.replace_existing = true;
self
}
pub const fn select(mut self) -> Self {
self.select = true;
self
}
fn into_command(self, session: &str, print_format: &str) -> Command {
let target = self
.index
.map_or_else(|| session.to_owned(), |index| format!("{session}:{index}"));
let mut command = Command::new("new-window")
.arg("-P")
.arg("-F")
.arg(print_format)
.arg("-t")
.arg(target);
if !self.select {
command = command.arg("-d");
}
match self.placement {
Some(WindowPlacement::Before) => command = command.arg("-b"),
Some(WindowPlacement::After) => command = command.arg("-a"),
None => {}
}
if self.replace_existing {
command = command.arg("-k");
}
if let Some(name) = self.name {
command = command.arg("-n").arg(name);
}
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(crate::window::assignment(&name, &value));
}
if let Some(shell_command) = self.command {
command = command.arg(shell_command);
}
command
}
}
impl<T: Into<OsString>> From<T> for NewWindowOptions {
fn from(name: T) -> Self {
Self::new(name)
}
}
impl fmt::Display for Session {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.id())
}
}