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::environment;
use crate::internal::listing::{self, Pushdown as _};
use crate::internal::options;
use crate::pane::Pane;
#[cfg(feature = "query")]
use crate::query::Filterable;
#[cfg(feature = "query")]
use crate::snapshot::SessionFields;
use crate::snapshot::SessionInfo;
use crate::target::{ServerIdentity, SessionId};
use crate::window::Window;
use crate::{Command, CommandResult, Error, IndexedHooks, ObjectKind, OptionValue, ReplaceMode};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EnvironmentEntry {
Set(TmuxText),
Removed,
}
#[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<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_or_empty(&self) -> Vec<Window> {
self.windows().await.unwrap_or_default()
}
pub async fn 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())
}
#[cfg(feature = "query")]
#[must_use]
pub async fn search_windows_or_empty<M: crate::query::Matcher<Window>>(
&self,
matcher: M,
) -> Vec<Window> {
self.search_windows(matcher).await.unwrap_or_default()
}
#[cfg(feature = "query")]
pub async fn search_windows<M: crate::query::Matcher<Window>>(
&self,
matcher: M,
) -> Result<Vec<Window>, Error> {
use crate::query::QueryIteratorExt as _;
let all = self.windows().await?;
Ok(all.iter().matching(matcher).cloned().collect())
}
pub async fn next_window(&self) -> Result<Option<Window>, Error> {
self.step("next-window").await
}
pub async fn previous_window(&self) -> Result<Option<Window>, Error> {
self.step("previous-window").await
}
pub async fn last_window(&self) -> Result<Option<Window>, Error> {
self.step("last-window").await
}
async fn step(&self, command: &'static str) -> Result<Option<Window>, Error> {
let target = self.id().to_string();
let result = self
.core
.execute(Command::new(command).arg("-t").arg(&target))
.await?;
if !result.success() {
let stderr = result.stderr_lossy();
if crate::error::NO_SUCH_NEIGHBOUR.contains(&stderr.trim_end()) {
return Ok(None);
}
return Err(Error::refused(
command,
result.exit_code(),
stderr.into_owned(),
Some(OsStr::new(&target)),
));
}
self.active_window().await
}
pub async fn active_window(&self) -> Result<Option<Window>, Error> {
Ok(self.windows().await?.into_iter().find(Window::is_active))
}
pub async fn panes_or_empty(&self) -> Vec<Pane> {
self.panes().await.unwrap_or_default()
}
pub async fn 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 options(&self) -> Result<BTreeMap<String, OptionValue>, Error> {
let target = self.id().to_string();
options::typed_all(&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 set_hooks(
&self,
name: &str,
hooks: &IndexedHooks,
replace: ReplaceMode,
) -> Result<(), Error> {
let target = self.id().to_string();
options::set_hooks(
&self.core,
options::Scope::Session(&target),
name,
hooks,
replace,
)
.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 lock(&self) -> Result<(), Error> {
listing::mutate(
&self.core,
"lock-session",
Command::new("lock-session")
.arg("-t")
.arg(self.id().to_string()),
)
.await
}
pub async fn detach_clients(&self) -> Result<(), Error> {
let target = self.id().to_string();
let result = self
.core
.execute(Command::new("detach-client").arg("-s").arg(&target))
.await?;
if result.success() {
return Ok(());
}
let stderr = result.stderr_lossy();
if stderr.trim_end() == crate::error::NO_CURRENT_CLIENT {
return Ok(());
}
Err(Error::refused(
"detach-client",
result.exit_code(),
stderr.into_owned(),
Some(OsStr::new(&target)),
))
}
pub async fn hooks(&self) -> Result<BTreeMap<String, IndexedHooks>, Error> {
let target = self.id().to_string();
options::hooks(&self.core, options::Scope::Session(&target)).await
}
pub async fn hook(&self, name: &str) -> Result<Option<IndexedHooks>, Error> {
let target = self.id().to_string();
options::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> {
environment::set(
&self.core,
environment::Scope::Session(self.id().as_ref()),
name,
value.into(),
)
.await
}
pub async fn environment(&self, name: &str) -> Result<Option<EnvironmentEntry>, Error> {
environment::get(
&self.core,
environment::Scope::Session(self.id().as_ref()),
name,
)
.await
}
pub async fn environment_all(&self) -> Result<BTreeMap<String, EnvironmentEntry>, Error> {
environment::all(&self.core, environment::Scope::Session(self.id().as_ref())).await
}
pub async fn hide_environment(&self, name: &str) -> Result<(), Error> {
environment::hide(
&self.core,
environment::Scope::Session(self.id().as_ref()),
name,
)
.await
}
pub async fn unset_environment(&self, name: &str) -> Result<(), Error> {
environment::unset(
&self.core,
environment::Scope::Session(self.id().as_ref()),
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
.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()
}
}
#[cfg(feature = "query")]
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())
}
}