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::refused(
list_command,
result.exit_code(),
stderr,
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),
}
}
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 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::refused(
command_name,
result.exit_code(),
result.stderr_lossy().into_owned(),
target.as_deref(),
));
}
hydrate(result.stdout())?
.into_iter()
.next()
.ok_or(Error::CommandFailed {
command: command_name,
exit_code: result.exit_code(),
stderr: String::from("tmux printed no object for a creating command"),
})
}
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(Error::refused(
command_name,
result.exit_code(),
result.stderr_lossy().into_owned(),
target.as_deref(),
))
}
#[cfg_attr(
not(feature = "tracing"),
expect(
unused_variables,
reason = "the cause has no sink when tracing is disabled"
)
)]
pub(crate) fn trace_discarded_cleanup(error: &Error) {
#[cfg(feature = "tracing")]
tracing::debug!(
error = %error,
"a scoped operation discarded a cleanup failure after its body failed",
);
}