use std::ffi::{OsStr, OsString};
use crate::error::ListingDecodeError;
use crate::formats::{FormatCodecError, FormatPlan, ListProfile};
use crate::internal::core::Core;
use crate::snapshot::{
ClientInfo, PaneProjection, SessionInfo, WindowProjection, hydrate_client_infos_from_stdout,
hydrate_pane_projections_from_stdout, hydrate_session_infos_from_stdout,
hydrate_window_projections_from_stdout, pane_projection_plan, window_projection_plan,
};
use crate::{Command, Error};
#[derive(Clone, Copy, Debug)]
pub(crate) enum Scope<'target> {
Server,
Target(&'target str),
SessionTarget(&'target str),
Unscoped,
}
impl<'target> Scope<'target> {
const fn target(self) -> Option<&'target str> {
match self {
Self::Target(target) | Self::SessionTarget(target) => Some(target),
Self::Server | Self::Unscoped => None,
}
}
fn apply(self, command: Command) -> Command {
match self {
Self::Server => command.arg("-a"),
Self::Target(target) => command.arg("-t").arg(OsString::from(target)),
Self::SessionTarget(target) => command.arg("-s").arg("-t").arg(OsString::from(target)),
Self::Unscoped => command,
}
}
}
pub(crate) trait Pushdown {
fn predicate(&self, field: &str) -> String;
}
macro_rules! pushdown_via_display {
($($type:ty),+ $(,)?) => {
$(impl Pushdown for $type {
fn predicate(&self, field: &str) -> String {
format!("#{{==:#{{{field}}},{self}}}")
}
})+
};
}
pushdown_via_display!(crate::SessionId, crate::WindowId, crate::PaneId, i32, u32,);
async fn list(
core: &Core,
list_command: &'static str,
scope: Scope<'_>,
filter: Option<&str>,
template: &str,
) -> Result<Vec<u8>, Error> {
let mut command = scope.apply(Command::new(list_command));
if let Some(filter) = filter {
command = command.arg("-f").arg(OsString::from(filter));
}
let result = core
.execute(command.arg("-F").arg(OsString::from(template)))
.await?;
if !result.success() {
let stderr = result.stderr_lossy().into_owned();
if scope.target().is_none() && stderr.trim_end() == crate::error::NO_CURRENT_TARGET {
return Ok(Vec::new());
}
return Err(Error::from_refused_result(
list_command,
&result,
scope.target().map(OsStr::new),
));
}
Ok(result.stdout().to_vec())
}
fn decode_error(list_command: &'static str) -> impl Fn(FormatCodecError) -> Error {
move |error| Error::DecodeListing {
list_command,
detail: ListingDecodeError::new(error),
}
}
#[cfg_attr(
not(feature = "tracing"),
expect(
unused_variables,
reason = "the cause has no sink when tracing is disabled"
)
)]
pub(crate) fn trace_discarded(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(crate) async fn sessions(core: &Core, filter: Option<&str>) -> Result<Vec<SessionInfo>, Error> {
const LIST_COMMAND: &str = "list-sessions";
let version = core.capabilities().await?.tmux_version().clone();
let plan = FormatPlan::for_profile(ListProfile::Sessions, &version);
let stdout = list(core, LIST_COMMAND, Scope::Unscoped, filter, plan.template()).await?;
hydrate_session_infos_from_stdout(&plan, &stdout).map_err(decode_error(LIST_COMMAND))
}
pub(crate) async fn windows(
core: &Core,
scope: Scope<'_>,
filter: Option<&str>,
) -> Result<Vec<WindowProjection>, Error> {
const LIST_COMMAND: &str = "list-windows";
let version = core.capabilities().await?.tmux_version().clone();
let plan = window_projection_plan(&version).map_err(decode_error(LIST_COMMAND))?;
let stdout = list(core, LIST_COMMAND, scope, filter, plan.template()).await?;
hydrate_window_projections_from_stdout(core.configuration().identity(), &plan, &stdout)
.map_err(decode_error(LIST_COMMAND))
}
pub(crate) async fn window_for_pane(
core: &Core,
pane: &crate::PaneId,
) -> Result<Option<WindowProjection>, Error> {
const LIST_COMMAND: &str = "list-panes";
let version = core.capabilities().await?.tmux_version().clone();
let plan = window_projection_plan(&version).map_err(decode_error(LIST_COMMAND))?;
let target = pane.to_string();
let filter = pane.predicate("pane_id");
let stdout = match list(
core,
LIST_COMMAND,
Scope::Target(&target),
Some(&filter),
plan.template(),
)
.await
{
Err(error) if error.is_object_gone() => return Ok(None),
result => result?,
};
Ok(
hydrate_window_projections_from_stdout(core.configuration().identity(), &plan, &stdout)
.map_err(decode_error(LIST_COMMAND))?
.into_iter()
.next(),
)
}
pub(crate) async fn panes(
core: &Core,
scope: Scope<'_>,
filter: Option<&str>,
) -> Result<Vec<PaneProjection>, Error> {
const LIST_COMMAND: &str = "list-panes";
let version = core.capabilities().await?.tmux_version().clone();
let plan = pane_projection_plan(&version).map_err(decode_error(LIST_COMMAND))?;
let stdout = list(core, LIST_COMMAND, scope, filter, plan.template()).await?;
hydrate_pane_projections_from_stdout(core.configuration().identity(), &plan, &stdout)
.map_err(decode_error(LIST_COMMAND))
}
pub(crate) async fn clients(core: &Core, filter: Option<&str>) -> Result<Vec<ClientInfo>, Error> {
const LIST_COMMAND: &str = "list-clients";
let version = core.capabilities().await?.tmux_version().clone();
let plan = FormatPlan::for_profile(ListProfile::Clients, &version);
let stdout = list(core, LIST_COMMAND, Scope::Unscoped, filter, plan.template()).await?;
hydrate_client_infos_from_stdout(&plan, &stdout).map_err(decode_error(LIST_COMMAND))
}
async fn create_one<T>(
core: &Core,
command_name: &'static str,
build: impl FnOnce(&str) -> Command,
template: &str,
hydrate: impl FnOnce(&[u8]) -> Result<Vec<T>, Error>,
) -> Result<T, Error> {
let command = build(template);
let target = command.target().map(OsStr::to_os_string);
let result = core.execute(command).await?;
if !result.success() {
return Err(Error::from_refused_result(
command_name,
&result,
target.as_deref(),
));
}
hydrate(result.stdout())
.map_err(|error| error.after_effect(command_name))?
.into_iter()
.next()
.ok_or_else(|| {
Error::CommandFailed {
command: command_name,
exit_code: result.exit_code(),
stderr: String::from("tmux printed no object for a creating command"),
}
.after_effect(command_name)
})
}
pub(crate) async fn create_session(
core: &Core,
build: impl FnOnce(&str) -> Command,
) -> Result<SessionInfo, Error> {
let version = core.capabilities().await?.tmux_version().clone();
let plan = FormatPlan::for_profile(ListProfile::Sessions, &version);
let template = plan.template().to_owned();
create_one(core, "new-session", build, &template, |stdout| {
hydrate_session_infos_from_stdout(&plan, stdout).map_err(decode_error("new-session"))
})
.await
}
pub(crate) async fn create_window(
core: &Core,
build: impl FnOnce(&str) -> Command,
) -> Result<WindowProjection, Error> {
let version = core.capabilities().await?.tmux_version().clone();
let plan = window_projection_plan(&version).map_err(decode_error("new-window"))?;
let template = plan.template().to_owned();
let identity = core.configuration().identity();
create_one(core, "new-window", build, &template, |stdout| {
hydrate_window_projections_from_stdout(identity, &plan, stdout)
.map_err(decode_error("new-window"))
})
.await
}
pub(crate) async fn create_pane(
core: &Core,
build: impl FnOnce(&str) -> Command,
) -> Result<PaneProjection, Error> {
let version = core.capabilities().await?.tmux_version().clone();
let plan = pane_projection_plan(&version).map_err(decode_error("split-window"))?;
let template = plan.template().to_owned();
let identity = core.configuration().identity();
create_one(core, "split-window", build, &template, |stdout| {
hydrate_pane_projections_from_stdout(identity, &plan, stdout)
.map_err(decode_error("split-window"))
})
.await
}
pub(crate) async fn mutate(
core: &Core,
command_name: &'static str,
command: Command,
) -> Result<(), Error> {
let target = command.target().map(OsStr::to_os_string);
let result = core.execute(command).await?;
if result.success() {
return Ok(());
}
Err(mutation_failure(command_name, &result, target.as_deref()))
}
fn mutation_failure(
command_name: &'static str,
result: &crate::CommandResult,
target: Option<&OsStr>,
) -> Error {
Error::from_refused_result(command_name, result, target)
}
#[cfg(test)]
mod tests {
use std::os::unix::process::ExitStatusExt as _;
use std::process::ExitStatus;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{create_session, mutation_failure};
use crate::command::{CommandRequest, CommandResult, ProcessStatus, RequestId};
use crate::internal::core::Core;
use crate::internal::executor::{DispatchFuture, Executor, ShutdownFuture};
use crate::{Command, Error, ErrorKind};
struct CreationExecutor {
calls: AtomicUsize,
stdout: &'static [u8],
}
impl Executor for CreationExecutor {
fn execute(&self, request: CommandRequest) -> DispatchFuture {
let call = self.calls.fetch_add(1, Ordering::SeqCst);
let stdout = if call == 0 {
b"tmux 3.7b\n".to_vec()
} else {
assert_eq!(call, 1, "one probe and one creating command");
self.stdout.to_vec()
};
DispatchFuture::new(async move {
Ok(CommandResult::new(
request.request_id(),
request.summary().clone(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(0)),
stdout,
Vec::new(),
))
})
}
fn shutdown(&self) -> ShutdownFuture {
ShutdownFuture::new(async { Ok(()) })
}
}
#[test]
fn sensitive_mutation_failure_withholds_tmux_output() {
let secret = "sentinel-mutation-secret";
let command = Command::new("set-option")
.arg("--")
.arg("mouse")
.sensitive_arg(secret);
let result = CommandResult::new(
RequestId::new(1),
command.summary(),
ProcessStatus::from_exit_status(ExitStatus::from_raw(1 << 8)),
Vec::new(),
format!("bad value: {secret}\n").into_bytes(),
);
let error = mutation_failure("set-option", &result, None);
assert!(matches!(&error, Error::CommandFailed { .. }));
let diagnostic = format!("{error:?} {error}");
assert!(!diagnostic.contains(secret), "{diagnostic}");
}
#[tokio::test]
async fn successful_creation_marks_decode_and_missing_object_failures() {
for stdout in [b"malformed\n".as_slice(), b"".as_slice()] {
let executor = Arc::new(CreationExecutor {
calls: AtomicUsize::new(0),
stdout,
});
let core = Core::from_executor_for_test(executor.clone());
let error = create_session(&core, |_format| Command::new("new-session"))
.await
.expect_err("tmux succeeded but did not describe the created session");
assert_eq!(executor.calls.load(Ordering::SeqCst), 2);
assert_eq!(error.kind(), ErrorKind::PartialEffect);
assert!(matches!(
error,
Error::AfterEffect {
operation: "new-session",
..
}
));
}
}
}