use std::collections::{BTreeMap, HashMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use crate::client::Client;
use crate::formats::TmuxText;
use crate::internal::core::{BuildContext, Core, CoreConfiguration, SocketSelection};
use crate::internal::environment;
#[cfg(test)]
use crate::internal::executor::Executor;
use crate::internal::listing::{self, Pushdown as _};
use crate::internal::options;
use crate::pane::Pane;
#[cfg(feature = "query")]
use crate::query::{Filterable, ManyRelation};
use crate::session::Session;
#[cfg(feature = "query")]
use crate::snapshot::{SessionFields, WindowFields};
use crate::window::Window;
use crate::{
Command, CommandChain, CommandResult, DispatchLimits, EngineCapabilities, EnvironmentEntry,
Error, IndexedHooks, ObjectKind, OptionValue, OutputLimits, PaneId, ReleaseSuffix,
ReleaseVersion, ReplaceMode, ServerConfigurationErrorKind, ServerGeneration, ServerIdentity,
SessionId, SparseValues, WindowId,
};
use crate::version::since::SERVER_ACCESS as SERVER_ACCESS_SINCE;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AccessMode {
ReadOnly,
Write,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AccessRule {
user: String,
mode: AccessMode,
}
impl AccessRule {
#[must_use]
pub fn user(&self) -> &str {
&self.user
}
#[must_use]
pub const fn mode(&self) -> AccessMode {
self.mode
}
}
use crate::version::since::PROMPT_HISTORY as PROMPT_HISTORY_SINCE;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PromptKind {
Command,
Search,
Target,
WindowTarget,
}
impl PromptKind {
const fn name(self) -> &'static str {
match self {
Self::Command => "command",
Self::Search => "search",
Self::Target => "target",
Self::WindowTarget => "window-target",
}
}
}
#[derive(Clone)]
pub struct Server {
core: Arc<Core>,
}
impl Server {
pub(crate) const fn from_core(core: Arc<Core>) -> Self {
Self { core }
}
pub fn new() -> Result<Self, Error> {
Self::builder().build()
}
pub fn builder() -> ServerBuilder {
ServerBuilder::new()
}
#[must_use]
pub fn identity(&self) -> &ServerIdentity {
self.core.configuration().identity()
}
#[must_use]
pub fn socket_path(&self) -> &Path {
self.identity().socket_path()
}
#[must_use]
pub fn socket_name(&self) -> Option<&OsStr> {
self.core.configuration().socket_name()
}
#[must_use]
pub fn config_file(&self) -> Option<&Path> {
self.core.configuration().config_file()
}
#[must_use]
pub fn colors(&self) -> Option<u16> {
self.core.configuration().colors()
}
#[must_use]
pub fn tmux_executable(&self) -> &OsStr {
self.core.configuration().executable()
}
#[must_use]
pub fn default_timeout(&self) -> Duration {
self.core.configuration().timeout()
}
pub async fn capabilities(&self) -> Result<&EngineCapabilities, Error> {
self.core.capabilities().await
}
pub async fn cmd(&self, command: Command) -> Result<CommandResult, Error> {
self.core.execute(command).await
}
pub async fn chain(&self, chain: CommandChain) -> Result<CommandResult, Error> {
self.core.execute_chain(chain).await
}
pub async fn shutdown(&self) -> Result<(), Error> {
self.core.shutdown().await
}
pub async fn sessions_or_empty(&self) -> Vec<Session> {
self.sessions().await.unwrap_or_else(|error| {
Self::trace_lenient_listing("list-sessions", &error);
Vec::new()
})
}
pub async fn windows_or_empty(&self) -> Vec<Window> {
self.windows().await.unwrap_or_else(|error| {
Self::trace_lenient_listing("list-windows", &error);
Vec::new()
})
}
pub async fn windows(&self) -> Result<Vec<Window>, Error> {
let projections = listing::windows(&self.core, listing::Scope::Server, None).await?;
Ok(projections
.into_iter()
.map(|projection| Window::new(Arc::clone(&self.core), projection))
.collect())
}
pub async fn panes_or_empty(&self) -> Vec<Pane> {
self.panes().await.unwrap_or_else(|error| {
Self::trace_lenient_listing("list-panes", &error);
Vec::new()
})
}
pub async fn panes(&self) -> Result<Vec<Pane>, Error> {
let projections = listing::panes(&self.core, listing::Scope::Server, None).await?;
Ok(projections
.into_iter()
.map(|projection| Pane::new(Arc::clone(&self.core), projection))
.collect())
}
pub async fn clients_or_empty(&self) -> Vec<Client> {
self.clients().await.unwrap_or_else(|error| {
Self::trace_lenient_listing("list-clients", &error);
Vec::new()
})
}
pub async fn clients(&self) -> Result<Vec<Client>, Error> {
let infos = listing::clients(&self.core, None).await?;
Ok(infos
.into_iter()
.map(|info| Client::new(Arc::clone(&self.core), info))
.collect())
}
pub fn from_env() -> Result<Self, Error> {
Self::from_env_value(std::env::var_os("TMUX"))
}
pub fn from_env_value(value: Option<impl Into<OsString>>) -> Result<Self, Error> {
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
let value: OsString = value.map(Into::into).ok_or_else(|| {
Error::invalid_server_configuration(ServerConfigurationErrorKind::NotInsideTmux)
})?;
let bytes = value.as_bytes();
let socket = bytes
.iter()
.position(|byte| *byte == b',')
.map(|index| &bytes[..index])
.filter(|socket| !socket.is_empty())
.ok_or_else(|| {
Error::invalid_server_configuration(
ServerConfigurationErrorKind::MalformedTmuxVariable,
)
})?;
Self::builder()
.socket_path(PathBuf::from(OsString::from_vec(socket.to_vec())))
.build()
}
pub async fn attached_sessions_or_empty(&self) -> Vec<Session> {
self.attached_sessions().await.unwrap_or_else(|error| {
Self::trace_lenient_listing("list-sessions", &error);
Vec::new()
})
}
pub async fn attached_sessions(&self) -> Result<Vec<Session>, Error> {
let mut sessions = self.sessions().await?;
sessions.retain(Session::is_attached);
Ok(sessions)
}
pub async fn new_session(
&self,
options: impl Into<NewSessionOptions>,
) -> Result<Session, Error> {
let options = options.into();
let info =
listing::create_session(&self.core, |format| options.into_command(format)).await?;
Ok(Session::new(Arc::clone(&self.core), info))
}
pub async fn kill(&self) -> Result<(), Error> {
let result = self.cmd(Command::new("kill-server")).await?;
if result.success() {
return Ok(());
}
if self.is_alive().await {
return Err(Error::CommandFailed {
command: "kill-server",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(())
}
pub async fn get_option(&self, name: &str) -> Result<Option<TmuxText>, Error> {
options::get(&self.core, options::Scope::Server, name).await
}
pub async fn option_names(&self) -> Result<Vec<String>, Error> {
options::names(&self.core, options::Scope::Server).await
}
pub async fn options(&self) -> Result<BTreeMap<String, OptionValue>, Error> {
options::typed_all(&self.core, options::Scope::Server).await
}
pub async fn set_option(&self, name: &str, value: impl Into<OsString>) -> Result<(), Error> {
options::set(&self.core, options::Scope::Server, name, value, false).await
}
pub async fn unset_option(&self, name: &str) -> Result<(), Error> {
options::unset(&self.core, options::Scope::Server, name).await
}
pub async fn get_global_option(&self, name: &str) -> Result<Option<TmuxText>, Error> {
options::get(&self.core, options::Scope::GlobalSession, name).await
}
pub async fn set_global_option(
&self,
name: &str,
value: impl Into<OsString>,
) -> Result<(), Error> {
options::set(
&self.core,
options::Scope::GlobalSession,
name,
value,
false,
)
.await
}
pub async fn set_environment(
&self,
name: &str,
value: impl Into<OsString>,
) -> Result<(), Error> {
environment::set(&self.core, environment::Scope::Global, name, value.into()).await
}
pub async fn environment(&self, name: &str) -> Result<Option<EnvironmentEntry>, Error> {
environment::get(&self.core, environment::Scope::Global, name).await
}
pub async fn environment_all(&self) -> Result<BTreeMap<String, EnvironmentEntry>, Error> {
environment::all(&self.core, environment::Scope::Global).await
}
pub async fn hide_environment(&self, name: &str) -> Result<(), Error> {
environment::hide(&self.core, environment::Scope::Global, name).await
}
pub async fn unset_environment(&self, name: &str) -> Result<(), Error> {
environment::unset(&self.core, environment::Scope::Global, name).await
}
pub async fn array_option(&self, name: &str) -> Result<SparseValues<TmuxText>, Error> {
Ok(SparseValues::from(
options::indexed(&self.core, options::Scope::GlobalSession, name).await?,
))
}
pub async fn set_array_option(
&self,
name: &str,
index: u32,
value: impl Into<OsString>,
) -> Result<(), Error> {
options::set(
&self.core,
options::Scope::GlobalSession,
&format!("{name}[{index}]"),
value,
false,
)
.await
}
pub async fn append_array_option(
&self,
name: &str,
index: u32,
value: impl Into<OsString>,
) -> Result<(), Error> {
options::set(
&self.core,
options::Scope::GlobalSession,
&format!("{name}[{index}]"),
value,
true,
)
.await
}
pub async fn unset_array_option(&self, name: &str, index: u32) -> Result<(), Error> {
options::unset(
&self.core,
options::Scope::GlobalSession,
&format!("{name}[{index}]"),
)
.await
}
pub async fn get_global_window_option(&self, name: &str) -> Result<Option<TmuxText>, Error> {
options::get(&self.core, options::Scope::GlobalWindow, name).await
}
pub async fn set_global_window_option(
&self,
name: &str,
value: impl Into<OsString>,
) -> Result<(), Error> {
options::set(&self.core, options::Scope::GlobalWindow, name, value, false).await
}
pub async fn set_hook(&self, name: &str, command: impl Into<OsString>) -> Result<(), Error> {
options::set_hook(&self.core, options::Scope::GlobalSession, name, command).await
}
pub async fn unset_hook(&self, name: &str) -> Result<(), Error> {
options::unset_hook(&self.core, options::Scope::GlobalSession, name).await
}
pub async fn set_hooks(
&self,
name: &str,
hooks: &IndexedHooks,
replace: ReplaceMode,
) -> Result<(), Error> {
options::set_hooks(
&self.core,
options::Scope::GlobalSession,
name,
hooks,
replace,
)
.await
}
pub async fn hooks(&self) -> Result<BTreeMap<String, IndexedHooks>, Error> {
options::hooks(&self.core, options::Scope::GlobalSession).await
}
pub async fn hook(&self, name: &str) -> Result<Option<IndexedHooks>, Error> {
options::hook(&self.core, options::Scope::GlobalSession, name).await
}
pub async fn set_buffer(
&self,
name: Option<&str>,
data: impl Into<OsString>,
) -> Result<(), Error> {
let mut command = Command::new("set-buffer");
if let Some(name) = name {
command = command.arg("-b").arg(OsString::from(name));
}
listing::mutate(&self.core, "set-buffer", command.sensitive_arg(data.into())).await
}
pub async fn buffer(&self, name: &str) -> Result<Option<Vec<u8>>, Error> {
let result = self
.cmd(
Command::new("show-buffer")
.arg("-b")
.arg(OsString::from(name)),
)
.await?;
if result.success() {
Ok(Some(result.stdout().to_vec()))
} else {
Ok(None)
}
}
pub async fn buffer_names(&self) -> Result<Vec<String>, Error> {
let result = self
.cmd(Command::new("list-buffers").arg("-F").arg("#{buffer_name}"))
.await?;
if !result.success() {
return Err(Error::CommandFailed {
command: "list-buffers",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(result
.stdout_lossy()
.lines()
.map(ToOwned::to_owned)
.collect())
}
pub async fn delete_buffer(&self, name: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"delete-buffer",
Command::new("delete-buffer")
.arg("-b")
.arg(OsString::from(name)),
)
.await
}
pub async fn bind_key(
&self,
table: &str,
key: &str,
command: impl Into<OsString>,
) -> Result<(), Error> {
listing::mutate(
&self.core,
"bind-key",
Command::new("bind-key")
.arg("-T")
.arg(OsString::from(table))
.arg(OsString::from(key))
.arg(command.into()),
)
.await
}
pub async fn unbind_key(&self, table: &str, key: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"unbind-key",
Command::new("unbind-key")
.arg("-T")
.arg(OsString::from(table))
.arg(OsString::from(key)),
)
.await
}
pub async fn key_bindings(&self, table: Option<&str>) -> Result<Vec<String>, Error> {
let mut command = Command::new("list-keys");
if let Some(table) = table {
command = command.arg("-T").arg(OsString::from(table));
}
let result = self.cmd(command).await?;
if !result.success() {
return Err(Error::CommandFailed {
command: "list-keys",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
Ok(result
.stdout_lossy()
.lines()
.map(ToOwned::to_owned)
.collect())
}
pub async fn format(&self, pane: Option<&Pane>, format: &str) -> Result<TmuxText, Error> {
let mut command = Command::new("display-message").arg("-p");
if let Some(pane) = pane {
command = command.arg("-t").arg(pane.id().to_string());
}
let result = self.cmd(command.arg(OsString::from(format))).await?;
if !result.success() {
return Err(Error::refused(
"display-message",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
));
}
let stdout = result.stdout();
let value = stdout.strip_suffix(b"\n").unwrap_or(stdout);
Ok(TmuxText::from(value.to_vec()))
}
pub(crate) async fn require(
&self,
capability: &'static str,
needs: ReleaseVersion,
) -> Result<(), Error> {
let found = self.capabilities().await?.tmux_version();
if found
.behavior_release()
.is_some_and(|release| release < needs)
{
return Err(Error::UnsupportedCapability {
capability,
needs,
found: found.clone(),
});
}
Ok(())
}
async fn refuse_if_defective(
&self,
capability: &'static str,
broken_in: ReleaseVersion,
fixed_in: ReleaseVersion,
) -> Result<(), Error> {
let found = self.capabilities().await?.tmux_version();
if found
.behavior_release()
.is_some_and(|release| release >= broken_in && release < fixed_in)
{
return Err(Error::CapabilityDefective {
capability,
found: found.clone(),
broken_in,
fixed_in,
});
}
Ok(())
}
pub async fn prompt_history(&self, kind: PromptKind) -> Result<Vec<TmuxText>, Error> {
self.require("prompt history", PROMPT_HISTORY_SINCE).await?;
let result = self
.cmd(
Command::new("show-prompt-history")
.arg("-T")
.arg(kind.name()),
)
.await?;
if !result.success() {
return Err(Error::refused(
"show-prompt-history",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
));
}
Ok(result
.stdout_lossy()
.lines()
.skip_while(|line| line.starts_with("History for "))
.filter(|line| !line.is_empty())
.map(|line| TmuxText::from(line.as_bytes().to_vec()))
.collect())
}
pub async fn clear_prompt_history(&self) -> Result<(), Error> {
self.require("prompt history", PROMPT_HISTORY_SINCE).await?;
listing::mutate(
&self.core,
"clear-prompt-history",
Command::new("clear-prompt-history"),
)
.await
}
pub async fn access_rules(&self) -> Result<Vec<AccessRule>, Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
let result = self.cmd(Command::new("server-access").arg("-l")).await?;
if !result.success() {
return Err(Error::refused(
"server-access",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
));
}
Ok(result
.stdout_lossy()
.lines()
.filter_map(|line| {
let (user, flag) = line.rsplit_once(' ')?;
let mode = match flag {
"(R)" => AccessMode::ReadOnly,
"(W)" => AccessMode::Write,
_ => return None,
};
Some(AccessRule {
user: user.to_owned(),
mode,
})
})
.collect())
}
pub async fn grant_access(&self, user: &str, mode: AccessMode) -> Result<(), Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
listing::mutate(
&self.core,
"server-access",
Command::new("server-access")
.arg("-a")
.arg(match mode {
AccessMode::ReadOnly => "-r",
AccessMode::Write => "-w",
})
.arg(OsString::from(user)),
)
.await
}
pub async fn revoke_access(&self, user: &str) -> Result<(), Error> {
self.require("the server access list", SERVER_ACCESS_SINCE)
.await?;
listing::mutate(
&self.core,
"server-access",
Command::new("server-access")
.arg("-d")
.arg(OsString::from(user)),
)
.await
}
pub async fn source_file(&self, path: impl Into<PathBuf>) -> Result<(), Error> {
listing::mutate(
&self.core,
"source-file",
Command::new("source-file").arg(path.into().into_os_string()),
)
.await
}
pub async fn with_session<T, E>(
&self,
options: impl Into<NewSessionOptions>,
operation: impl AsyncFnOnce(&Session) -> Result<T, E>,
) -> Result<T, E>
where
E: From<Error>,
{
let created = self.new_session(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 run_shell(&self, command: impl Into<OsString>) -> Result<Vec<TmuxText>, Error> {
self.refuse_if_defective(
"run-shell output",
ReleaseVersion::new(3, 3, ReleaseSuffix::FINAL),
ReleaseVersion::new(3, 5, ReleaseSuffix::FINAL),
)
.await?;
let result = self
.cmd(Command::new("run-shell").sensitive_arg(command.into()))
.await?;
if !result.success() {
return Err(Error::CommandFailed {
command: "run-shell",
exit_code: result.exit_code(),
stderr: result.stderr_lossy().into_owned(),
});
}
let stdout = result.stdout();
let stdout = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if stdout.is_empty() {
return Ok(Vec::new());
}
Ok(stdout
.split(|byte| *byte == b'\n')
.map(|line| TmuxText::from(line.to_vec()))
.collect())
}
pub async fn spawn_shell(&self, command: impl Into<OsString>) -> Result<(), Error> {
listing::mutate(
&self.core,
"run-shell",
Command::new("run-shell")
.arg("-b")
.sensitive_arg(command.into()),
)
.await
}
pub async fn signal_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-S")
.arg(OsString::from(channel)),
)
.await
}
pub async fn lock_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-L")
.arg(OsString::from(channel)),
)
.await
}
pub async fn unlock_channel(&self, channel: &str) -> Result<(), Error> {
listing::mutate(
&self.core,
"wait-for",
Command::new("wait-for")
.arg("-U")
.arg(OsString::from(channel)),
)
.await
}
pub async fn start(&self) -> Result<(), Error> {
listing::mutate(&self.core, "start-server", Command::new("start-server")).await
}
pub async fn display_popup(
&self,
client: Option<&Client>,
command: impl Into<OsString>,
) -> Result<(), Error> {
let mut popup = Command::new("display-popup").arg("-E");
if let Some(client) = client {
popup = popup
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
listing::mutate(&self.core, "display-popup", popup.arg(command.into())).await
}
pub async fn display_menu(
&self,
client: Option<&Client>,
title: &str,
items: impl IntoIterator<Item = (String, String, String)>,
) -> Result<(), Error> {
let mut menu = Command::new("display-menu")
.arg("-T")
.arg(OsString::from(title));
if let Some(client) = client {
menu = menu
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
for (label, key, command) in items {
menu = menu
.arg(OsString::from(label))
.arg(OsString::from(key))
.arg(OsString::from(command));
}
listing::mutate(&self.core, "display-menu", menu).await
}
pub async fn command_prompt(
&self,
client: Option<&Client>,
prompt: Option<&str>,
command: impl Into<OsString>,
) -> Result<(), Error> {
let mut request = Command::new("command-prompt");
if let Some(client) = client {
request = request
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
if let Some(prompt) = prompt {
request = request.arg("-p").arg(OsString::from(prompt));
}
listing::mutate(&self.core, "command-prompt", request.arg(command.into())).await
}
pub async fn choose(&self, chooser: Chooser, client: Option<&Client>) -> Result<(), Error> {
let name = chooser.command();
let mut request = Command::new(name);
if let Some(client) = client {
request = request
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
listing::mutate(&self.core, name, request).await
}
pub async fn find_window(&self, client: Option<&Client>, search: &str) -> Result<(), Error> {
let mut request = Command::new("find-window");
if let Some(client) = client {
request = request
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
listing::mutate(
&self.core,
"find-window",
request.arg(OsString::from(search)),
)
.await
}
pub async fn display_panes(&self, client: Option<&Client>) -> Result<(), Error> {
let mut request = Command::new("display-panes");
if let Some(client) = client {
request = request
.arg("-t")
.arg(client.name().to_string_lossy().into_owned());
}
listing::mutate(&self.core, "display-panes", request).await
}
pub async fn typed_option(&self, name: &str) -> Result<Option<OptionValue>, Error> {
Ok(options::get(&self.core, options::Scope::Server, name)
.await?
.map(|value| OptionValue::decode(name, value)))
}
pub async fn typed_global_option(&self, name: &str) -> Result<Option<OptionValue>, Error> {
Ok(
options::get(&self.core, options::Scope::GlobalSession, name)
.await?
.map(|value| OptionValue::decode(name, value)),
)
}
pub async fn session(&self, name: impl AsRef<[u8]>) -> Result<Option<Session>, Error> {
let name = name.as_ref();
Ok(self
.sessions()
.await?
.into_iter()
.find(|session| session.name() == name))
}
pub async fn generation(&self) -> Result<ServerGeneration, Error> {
let answer = self.format(None, "#{pid} #{start_time}").await?;
let text = answer.to_string_lossy();
let mut parts = text.split_whitespace();
let parsed = parts
.next()
.and_then(|pid| pid.parse::<u32>().ok())
.zip(parts.next().and_then(|start| start.parse::<i64>().ok()));
let Some((pid, start_time)) = parsed else {
return Err(Error::UnreadableFormatValue {
format: "#{pid} #{start_time}",
detail: crate::IdParseError::new('#'),
});
};
Ok(ServerGeneration { pid, start_time })
}
pub async fn require_generation(&self, expected: ServerGeneration) -> Result<(), Error> {
let found = self.generation().await?;
if found == expected {
return Ok(());
}
Err(Error::ServerGenerationChanged { expected, found })
}
pub async fn session_by_id(&self, id: &SessionId) -> Result<Option<Session>, Error> {
let infos = listing::sessions(&self.core, Some(&id.predicate("session_id"))).await?;
Ok(infos
.into_iter()
.next()
.map(|info| Session::new(Arc::clone(&self.core), info)))
}
pub async fn window_by_id(&self, id: &WindowId) -> Result<Option<Window>, Error> {
let projections = listing::windows(
&self.core,
listing::Scope::Server,
Some(&id.predicate("window_id")),
)
.await?;
Ok(projections
.into_iter()
.min_by_key(|projection| {
(
!projection.link().is_active(),
projection.link().identity().window_index(),
)
})
.map(|projection| Window::new(Arc::clone(&self.core), projection)))
}
pub async fn pane_by_id(&self, id: &PaneId) -> Result<Option<Pane>, Error> {
let projections = listing::panes(
&self.core,
listing::Scope::Server,
Some(&id.predicate("pane_id")),
)
.await?;
Ok(projections
.into_iter()
.next()
.map(|projection| Pane::new(Arc::clone(&self.core), projection)))
}
pub async fn client(&self, name: impl AsRef<[u8]>) -> Result<Option<Client>, Error> {
let name = name.as_ref();
Ok(self
.clients()
.await?
.into_iter()
.find(|client| client.name() == name))
}
pub async fn hierarchy(&self) -> Result<Vec<SessionTree>, Error> {
let (sessions, windows, panes) =
tokio::try_join!(self.sessions(), self.windows(), self.panes(),)?;
let mut seen = HashSet::new();
let mut panes_by_window: HashMap<u32, Vec<Pane>> = HashMap::new();
for pane in panes {
if !seen.insert(pane.id().number()) {
continue;
}
panes_by_window
.entry(pane.window_id().number())
.or_default()
.push(pane);
}
let mut windows_by_session: HashMap<u32, Vec<WindowTree>> = HashMap::new();
for window in windows {
let panes = panes_by_window
.get(&window.id().number())
.cloned()
.unwrap_or_default();
windows_by_session
.entry(window.session_id().number())
.or_default()
.push(WindowTree { window, panes });
}
Ok(sessions
.into_iter()
.map(|session| {
let windows = windows_by_session
.remove(&session.id().number())
.unwrap_or_default();
SessionTree { session, windows }
})
.collect())
}
pub async fn is_alive(&self) -> bool {
self.check_alive().await.is_ok()
}
pub async fn check_alive(&self) -> Result<(), Error> {
let result = self.cmd(Command::new("list-sessions")).await?;
if result.success() {
return Ok(());
}
Err(Error::ObjectGone {
kind: ObjectKind::Session,
id: self.identity().socket_path().display().to_string(),
})
}
pub async fn has_session(&self, name: impl AsRef<[u8]>) -> Result<bool, Error> {
let name = name.as_ref();
Ok(self
.sessions()
.await?
.iter()
.any(|session| session.name() == name))
}
#[cfg_attr(
not(feature = "tracing"),
expect(
unused_variables,
reason = "the cause has no sink when tracing is disabled"
)
)]
fn trace_lenient_listing(list_command: &'static str, error: &Error) {
#[cfg(feature = "tracing")]
tracing::debug!(
list_command,
error = %error,
"a lenient listing discarded a failure and returned empty",
);
}
pub async fn sessions(&self) -> Result<Vec<Session>, Error> {
let infos = listing::sessions(&self.core, None).await?;
Ok(infos
.into_iter()
.map(|info| Session::new(Arc::clone(&self.core), info))
.collect())
}
#[cfg(test)]
fn from_executor_for_test(executor: Arc<dyn Executor>) -> Self {
Self {
core: Arc::new(Core::from_executor_for_test(executor)),
}
}
}
impl PartialEq for Server {
fn eq(&self, other: &Self) -> bool {
self.identity() == other.identity()
}
}
impl Eq for Server {}
impl Hash for Server {
fn hash<H: Hasher>(&self, state: &mut H) {
self.identity().hash(state);
}
}
impl fmt::Debug for Server {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Server")
.field("identity", self.identity())
.finish_non_exhaustive()
}
}
#[must_use = "a server builder has no effect until build is called"]
pub struct ServerBuilder {
socket_name: Option<OsString>,
socket_path: Option<PathBuf>,
config_file: Option<PathBuf>,
colors: Option<u16>,
executable: OsString,
timeout: Duration,
output_limits: OutputLimits,
dispatch_limits: DispatchLimits,
#[cfg(feature = "test-support")]
prevent_server_start: bool,
}
impl fmt::Debug for ServerBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ServerBuilder")
.field(
"socket_name",
&self.socket_name.as_ref().map(|_| "<redacted>"),
)
.field(
"socket_path",
&self.socket_path.as_ref().map(|_| "<redacted>"),
)
.field(
"config_file",
&self.config_file.as_ref().map(|_| "<redacted>"),
)
.field("colors", &self.colors)
.field("timeout", &self.timeout)
.field("output_limits", &self.output_limits)
.field("dispatch_limits", &self.dispatch_limits)
.finish_non_exhaustive()
}
}
impl ServerBuilder {
fn new() -> Self {
Self {
socket_name: None,
socket_path: None,
config_file: None,
colors: None,
executable: OsString::from("tmux"),
timeout: CoreConfiguration::default_timeout(),
output_limits: OutputLimits::default(),
dispatch_limits: DispatchLimits::default(),
#[cfg(feature = "test-support")]
prevent_server_start: false,
}
}
#[must_use = "use the returned builder to retain the limits"]
pub const fn output_limits(mut self, limits: OutputLimits) -> Self {
self.output_limits = limits;
self
}
#[must_use = "use the returned builder to retain the limits"]
pub const fn dispatch_limits(mut self, limits: DispatchLimits) -> Self {
self.dispatch_limits = limits;
self
}
#[must_use = "use the returned builder to retain the socket name"]
pub fn socket_name(mut self, name: impl Into<OsString>) -> Self {
self.socket_name = Some(name.into());
self
}
#[must_use = "use the returned builder to retain the socket path"]
pub fn socket_path(mut self, path: impl Into<PathBuf>) -> Self {
self.socket_path = Some(path.into());
self
}
#[must_use = "use the returned builder to retain the config path"]
pub fn config_file(mut self, path: impl Into<PathBuf>) -> Self {
self.config_file = Some(path.into());
self
}
#[must_use = "use the returned builder to retain the color mode"]
pub const fn colors(mut self, colors: u16) -> Self {
self.colors = Some(colors);
self
}
#[must_use = "use the returned builder to retain the executable"]
pub fn tmux_executable(mut self, executable: impl Into<OsString>) -> Self {
self.executable = executable.into();
self
}
#[must_use = "use the returned builder to retain the timeout"]
pub const fn default_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[cfg(feature = "test-support")]
pub(crate) const fn prevent_server_start(mut self) -> Self {
self.prevent_server_start = true;
self
}
pub fn build(self) -> Result<Server, Error> {
let selection = match (self.socket_name, self.socket_path) {
(Some(name), None) => SocketSelection::Name(name),
(None, Some(path)) => SocketSelection::Path(path),
(None, None) => SocketSelection::Automatic,
(Some(_), Some(_)) => {
return Err(Error::invalid_server_configuration(
ServerConfigurationErrorKind::ConflictingSocketSelectors,
));
}
};
let configuration = CoreConfiguration::resolve(
&selection,
self.config_file,
self.colors,
self.executable,
self.timeout,
BuildContext::capture(),
)
.map_err(Error::invalid_server_configuration)?
.with_limits(self.output_limits, self.dispatch_limits);
#[cfg(feature = "test-support")]
let configuration = if self.prevent_server_start {
configuration.prevent_server_start()
} else {
configuration
};
Ok(Server {
core: Arc::new(Core::new(configuration)),
})
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::{Notify, watch};
use super::Server;
use crate::Error;
use crate::command::CommandRequest;
use crate::internal::executor::{DispatchFuture, Executor, ShutdownFuture};
struct BlockingShutdownExecutor {
closed: AtomicBool,
shutdown_started: Notify,
release: watch::Receiver<bool>,
}
impl Executor for BlockingShutdownExecutor {
fn execute(&self, request: CommandRequest) -> DispatchFuture {
let closed = self.closed.load(Ordering::SeqCst);
DispatchFuture::new(async move {
assert!(
closed,
"test dispatch occurs only after shutdown closes admission"
);
Err(Error::executor_shutdown(
request.request_id().get(),
request.summary().clone(),
))
})
}
fn shutdown(&self) -> ShutdownFuture {
self.closed.store(true, Ordering::SeqCst);
self.shutdown_started.notify_one();
let mut release = self.release.clone();
ShutdownFuture::new(async move {
while !*release.borrow() {
release
.changed()
.await
.expect("test release sender remains alive");
}
Ok(())
})
}
}
#[tokio::test]
async fn aborting_one_server_shutdown_keeps_all_clones_closed_until_later_completion() {
let (release_sender, release_receiver) = watch::channel(false);
let executor = Arc::new(BlockingShutdownExecutor {
closed: AtomicBool::new(false),
shutdown_started: Notify::new(),
release: release_receiver,
});
let server = Server::from_executor_for_test(executor.clone());
let started = executor.shutdown_started.notified();
let shutdown_server = server.clone();
let shutdown = tokio::spawn(async move { shutdown_server.shutdown().await });
started.await;
shutdown.abort();
assert!(
shutdown
.await
.expect_err("shutdown task was aborted")
.is_cancelled()
);
assert!(matches!(
server.cmd(crate::Command::new("display-message")).await,
Err(Error::ExecutorShutdown { .. })
));
release_sender
.send(true)
.expect("test release receiver remains alive");
server
.clone()
.shutdown()
.await
.expect("later clone completes shutdown");
}
}
#[must_use = "options describe a session but do not create one"]
#[derive(Clone, Debug)]
pub struct NewSessionOptions {
name: OsString,
start_directory: Option<PathBuf>,
window_name: Option<OsString>,
command: Option<OsString>,
width: Option<u32>,
height: Option<u32>,
}
impl NewSessionOptions {
pub fn new(name: impl Into<OsString>) -> Self {
Self {
name: name.into(),
start_directory: None,
window_name: None,
command: None,
width: None,
height: None,
}
}
pub fn start_directory(mut self, directory: impl Into<PathBuf>) -> Self {
self.start_directory = Some(directory.into());
self
}
pub fn window_name(mut self, name: impl Into<OsString>) -> Self {
self.window_name = Some(name.into());
self
}
pub fn command(mut self, command: impl Into<OsString>) -> Self {
self.command = Some(command.into());
self
}
pub fn size(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
fn into_command(self, print_format: &str) -> Command {
let mut command = Command::new("new-session")
.arg("-d")
.arg("-P")
.arg("-F")
.arg(print_format)
.arg("-s")
.arg(self.name);
if let Some(directory) = self.start_directory {
command = command.arg("-c").arg(directory.into_os_string());
}
if let Some(name) = self.window_name {
command = command.arg("-n").arg(name);
}
if let (Some(width), Some(height)) = (self.width, self.height) {
command = command
.arg("-x")
.arg(width.to_string())
.arg("-y")
.arg(height.to_string());
}
if let Some(shell_command) = self.command {
command = command.arg(shell_command);
}
command
}
}
impl<T: Into<OsString>> From<T> for NewSessionOptions {
fn from(name: T) -> Self {
Self::new(name)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Chooser {
Tree,
Client,
Buffer,
Customize,
}
impl Chooser {
const fn command(self) -> &'static str {
match self {
Self::Tree => "choose-tree",
Self::Client => "choose-client",
Self::Buffer => "choose-buffer",
Self::Customize => "customize-mode",
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct SessionTree {
pub session: Session,
pub windows: Vec<WindowTree>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct WindowTree {
pub window: Window,
pub panes: Vec<Pane>,
}
#[cfg(feature = "query")]
#[non_exhaustive]
pub struct SessionTreeFields {
pub session: SessionFields<SessionTree>,
pub windows: ManyRelation<SessionTree, WindowTree>,
}
#[cfg(feature = "query")]
impl fmt::Debug for SessionTreeFields {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SessionTreeFields")
.finish_non_exhaustive()
}
}
#[cfg(feature = "query")]
#[non_exhaustive]
pub struct WindowTreeFields {
pub window: WindowFields<WindowTree>,
pub panes: ManyRelation<WindowTree, Pane>,
}
#[cfg(feature = "query")]
impl fmt::Debug for WindowTreeFields {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WindowTreeFields")
.finish_non_exhaustive()
}
}
#[cfg(feature = "query")]
const WINDOWS_RELATION: &str = "windows";
#[cfg(feature = "query")]
const PANES_RELATION: &str = "panes";
#[cfg(feature = "query")]
impl Filterable for SessionTree {
type Fields = SessionTreeFields;
const FILTER_TARGET: &'static str = "session_tree";
fn filter_fields() -> Self::Fields {
Self::Fields {
session: SessionFields::for_target(Self::FILTER_TARGET),
windows: crate::query::__private::many_relation(Self::FILTER_TARGET, WINDOWS_RELATION),
}
}
fn __filter_matches(&self, predicate: &crate::query::__private::Predicate) -> bool {
if predicate.field() == WINDOWS_RELATION {
return predicate.matches_many(&self.windows);
}
self.session.__filter_matches(predicate)
}
fn __filter_validate(
predicate: &crate::query::__private::Predicate,
) -> Result<(), crate::query::FilterExpressionError> {
if predicate.field() == WINDOWS_RELATION {
return predicate.validate_many::<WindowTree>();
}
<Session as Filterable>::__filter_validate(predicate)
}
}
#[cfg(feature = "query")]
impl Filterable for WindowTree {
type Fields = WindowTreeFields;
const FILTER_TARGET: &'static str = "window_tree";
fn filter_fields() -> Self::Fields {
Self::Fields {
window: WindowFields::for_target(Self::FILTER_TARGET),
panes: crate::query::__private::many_relation(Self::FILTER_TARGET, PANES_RELATION),
}
}
fn __filter_matches(&self, predicate: &crate::query::__private::Predicate) -> bool {
if predicate.field() == PANES_RELATION {
return predicate.matches_many(&self.panes);
}
self.window.__filter_matches(predicate)
}
fn __filter_validate(
predicate: &crate::query::__private::Predicate,
) -> Result<(), crate::query::FilterExpressionError> {
if predicate.field() == PANES_RELATION {
return predicate.validate_many::<Pane>();
}
<Window as Filterable>::__filter_validate(predicate)
}
}