mod settings;
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::scoped;
use crate::pane::Pane;
#[cfg(feature = "query")]
use crate::query::{FilterSchema, 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, ObjectKind};
#[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_else(|error| {
listing::trace_discarded("list-windows", &error);
Vec::new()
})
}
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_else(|error| {
listing::trace_discarded("list-windows", &error);
Vec::new()
})
}
#[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::from_refused_result(
command,
&result,
Some(OsStr::new(&target)),
));
}
let active = self
.active_window()
.await
.map_err(|error| error.after_effect(command))?;
active
.ok_or_else(|| {
Error::ObjectGone {
kind: ObjectKind::Session,
id: target,
}
.after_effect(command)
})
.map(Some)
}
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_else(|error| {
listing::trace_discarded("list-panes", &error);
Vec::new()
})
}
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("--")
.arg(name.into()),
)
.await?;
self.refresh()
.await
.map_err(|error| error.after_effect("rename-session"))?;
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 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 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::from_refused_result(
"detach-client",
&result,
Some(OsStr::new(&target)),
))
}
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 session = self.clone();
let options = options.into();
scoped::run(
"with-window",
async move { session.new_window(options).await },
Window::kill,
operation,
)
.await
}
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)
}
}
#[cfg(feature = "query")]
impl FilterSchema for Session {
fn __filter_schema() -> crate::query::__private::FilterSchemaDescriptor {
<SessionInfo as FilterSchema>::__filter_schema()
}
}
#[must_use = "options describe a window but do not create one"]
#[derive(Clone)]
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,
}
impl fmt::Debug for NewWindowOptions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("NewWindowOptions")
.field("has_name", &self.name.is_some())
.field("has_start_directory", &self.start_directory.is_some())
.field("has_command", &self.command.is_some())
.field("index", &self.index)
.field("placement", &self.placement)
.field("environment_count", &self.environment.len())
.field("replace_existing", &self.replace_existing)
.field("select", &self.select)
.finish()
}
}
#[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")
.sensitive_arg(crate::window::assignment(&name, &value));
}
if let Some(shell_command) = self.command {
command = command.sensitive_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())
}
}
#[cfg(test)]
mod option_tests {
use super::NewWindowOptions;
#[test]
fn new_window_options_redact_process_inputs() {
let secret = "sentinel-window-process";
let options = NewWindowOptions::new("work")
.environment("TOKEN", secret)
.command(secret);
assert!(!format!("{options:?}").contains(secret));
let summary = options.into_command("$1", "#{window_id}").summary();
assert_eq!(summary.sensitive_argument_count(), 2);
assert!(!summary.to_string().contains(secret));
}
}