use std::collections::BTreeMap;
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt as _;
use crate::formats::TmuxText;
use crate::hooks::IndexedHooks;
use crate::hooks::ReplaceMode;
use crate::internal::core::Core;
use crate::options::{OptionScope, OptionValue};
use crate::{Command, CommandChain, Error};
#[derive(Clone, Copy, Debug)]
pub(crate) enum Scope<'target> {
Server,
GlobalSession,
GlobalWindow,
Session(&'target str),
Window(&'target str),
Pane(&'target str),
}
impl Scope<'_> {
fn option_scope(self) -> OptionScope {
match self {
Self::Server => OptionScope::Server,
Self::GlobalSession | Self::Session(_) => OptionScope::Session,
Self::GlobalWindow | Self::Window(_) => OptionScope::Window,
Self::Pane(_) => OptionScope::Pane,
}
}
fn apply(self, command: Command) -> Command {
match self {
Self::Server => command.arg("-s"),
Self::GlobalSession => command.arg("-g"),
Self::GlobalWindow => command.arg("-w").arg("-g"),
Self::Session(target) => command.arg("-t").arg(OsString::from(target)),
Self::Window(target) => command.arg("-w").arg("-t").arg(OsString::from(target)),
Self::Pane(target) => command.arg("-p").arg("-t").arg(OsString::from(target)),
}
}
}
pub(crate) async fn get(
core: &Core,
scope: Scope<'_>,
name: &str,
) -> Result<Option<TmuxText>, Error> {
let command = scope
.apply(Command::new("show-options"))
.arg("-v")
.arg("--")
.arg(OsString::from(name));
let result = core.execute(command).await?;
if !result.success() {
let failure = Error::from_refused_result("show-options", &result, None);
if name.starts_with('@')
&& matches!(
failure,
Error::OptionRejected {
kind: crate::OptionErrorKind::Unknown,
..
}
)
{
return Ok(None);
}
return Err(failure);
}
let stdout = result.stdout();
let value = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if value.is_empty() {
return Ok(None);
}
Ok(Some(TmuxText::from(value.to_vec())))
}
pub(crate) async fn names(core: &Core, scope: Scope<'_>) -> Result<Vec<String>, Error> {
let result = core
.execute(scope.apply(Command::new("show-options")))
.await?;
if !result.success() {
return Err(Error::from_refused_result("show-options", &result, None));
}
Ok(result
.stdout_lossy()
.lines()
.filter_map(|line| line.split_whitespace().next())
.map(ToOwned::to_owned)
.collect())
}
pub(crate) async fn set(
core: &Core,
scope: Scope<'_>,
name: &str,
value: impl Into<OsString>,
append: bool,
) -> Result<(), Error> {
let mut command = scope.apply(Command::new("set-option"));
if append {
command = command.arg("-a");
}
ensure_scope(core, scope, name).await?;
run(
core,
"set-option",
Some(name),
command
.arg("--")
.arg(OsString::from(name))
.sensitive_arg(value.into()),
)
.await
}
pub(crate) async fn unset(core: &Core, scope: Scope<'_>, name: &str) -> Result<(), Error> {
ensure_scope(core, scope, name).await?;
run(
core,
"set-option",
None,
scope
.apply(Command::new("set-option"))
.arg("-u")
.arg("--")
.arg(OsString::from(name)),
)
.await
}
pub(crate) async fn set_hook(
core: &Core,
scope: Scope<'_>,
name: &str,
command_text: impl Into<OsString>,
) -> Result<(), Error> {
ensure_scope(core, scope, name).await?;
let slot = if name.contains('[') {
OsString::from(name)
} else {
OsString::from(format!("{name}[0]"))
};
run(
core,
"set-hook",
None,
scope
.apply(Command::new("set-hook"))
.arg("--")
.arg(slot)
.sensitive_arg(command_text.into()),
)
.await
}
pub(crate) async fn unset_hook(core: &Core, scope: Scope<'_>, name: &str) -> Result<(), Error> {
ensure_scope(core, scope, name).await?;
run(
core,
"set-hook",
None,
scope
.apply(Command::new("set-hook"))
.arg("-u")
.arg("--")
.arg(OsString::from(name)),
)
.await
}
const LATE_SCOPES: &[(&str, OptionScope, crate::version::ReleaseVersion)] = &[
(
"pane-border-format",
OptionScope::Pane,
crate::version::since::PANE_BORDER_FORMAT_PER_PANE,
),
(
"pane-active-border-style",
OptionScope::Pane,
crate::version::since::PANE_BORDER_STYLE_PER_PANE,
),
(
"pane-border-style",
OptionScope::Pane,
crate::version::since::PANE_BORDER_STYLE_PER_PANE,
),
];
async fn ensure_scope(core: &Core, scope: Scope<'_>, name: &str) -> Result<(), Error> {
if name.starts_with('@') {
return Ok(());
}
let Some(schema) = crate::option_schema(name) else {
return Ok(());
};
let requested = scope.option_scope();
if !schema.accepts(requested) {
return Err(Error::OptionScopeMismatch {
option: schema.name().to_owned(),
requested,
declared: schema.scopes(),
});
}
for (option, late, needs) in LATE_SCOPES {
if *option == schema.name() && *late == requested {
let found = core.capabilities().await?.tmux_version();
if !found.meets(needs) {
return Err(Error::UnsupportedCapability {
capability: option,
needs: *needs,
found: found.clone(),
});
}
}
}
Ok(())
}
async fn run(
core: &Core,
command_name: &'static str,
option_name: Option<&str>,
command: Command,
) -> Result<(), Error> {
let result = core.execute(command).await?;
if result.success() {
return Ok(());
}
Err(mutation_failure(command_name, option_name, &result))
}
fn mutation_failure(
command_name: &'static str,
option_name: Option<&str>,
result: &crate::CommandResult,
) -> Error {
let exit_code = result.exit_code();
if result.command().sensitive_argument_count() == 0 {
return Error::from_refused_result(command_name, result, None);
}
let failure = Error::refused(
command_name,
exit_code,
result.stderr_lossy().into_owned(),
None,
);
match (option_name, failure) {
(Some(name), Error::OptionRejected { kind, .. }) => Error::OptionRejected {
kind,
detail: name.to_owned(),
},
(Some(name), Error::CommandFailed { .. }) => Error::OptionRejected {
kind: crate::OptionErrorKind::BadValue,
detail: name.to_owned(),
},
_ => Error::refused_withheld(command_name, exit_code),
}
}
pub(crate) async fn hook_slots(core: &Core, scope: Scope<'_>) -> Result<Vec<String>, Error> {
let result = core
.execute(scope.apply(Command::new("show-hooks")))
.await?;
if !result.success() {
return Err(Error::from_refused_result("show-hooks", &result, None));
}
Ok(result
.stdout_lossy()
.lines()
.filter_map(|line| line.split_whitespace().next())
.filter(|slot| slot.contains('['))
.map(ToOwned::to_owned)
.collect())
}
pub(crate) fn split_slot(slot: &str) -> Option<(&str, u32)> {
let (name, rest) = slot.split_once('[')?;
let index = rest.strip_suffix(']')?.parse().ok()?;
Some((name, index))
}
pub(crate) async fn hooks(
core: &Core,
scope: Scope<'_>,
) -> Result<BTreeMap<String, IndexedHooks>, Error> {
let mut collected: BTreeMap<String, BTreeMap<u32, TmuxText>> = BTreeMap::new();
for slot in hook_slots(core, scope).await? {
let Some((name, index)) = split_slot(&slot) else {
continue;
};
if let Some(value) = get(core, scope, &slot).await? {
collected
.entry(name.to_owned())
.or_default()
.insert(index, value);
}
}
Ok(collected
.into_iter()
.map(|(name, entries)| (name, IndexedHooks::from_entries(entries)))
.collect())
}
pub(crate) async fn indexed(
core: &Core,
scope: Scope<'_>,
name: &str,
) -> Result<BTreeMap<u32, TmuxText>, Error> {
let mut entries = BTreeMap::new();
for slot in slots_of(core, scope, name).await? {
let Some((_, index)) = split_slot(&slot) else {
continue;
};
if let Some(value) = get(core, scope, &slot).await? {
entries.insert(index, value);
}
}
Ok(entries)
}
async fn slots_of(core: &Core, scope: Scope<'_>, name: &str) -> Result<Vec<String>, Error> {
let result = core
.execute(
scope
.apply(Command::new("show-options"))
.arg("--")
.arg(OsString::from(name)),
)
.await?;
if !result.success() {
return Err(Error::from_refused_result("show-options", &result, None));
}
Ok(result
.stdout_lossy()
.lines()
.filter_map(|line| line.split_whitespace().next())
.filter(|slot| split_slot(slot).is_some_and(|(slot_name, _)| slot_name == name))
.map(ToOwned::to_owned)
.collect())
}
pub(crate) async fn hook(
core: &Core,
scope: Scope<'_>,
name: &str,
) -> Result<Option<IndexedHooks>, Error> {
let mut entries = BTreeMap::new();
for slot in slots_of(core, scope, name).await? {
let Some((_, index)) = split_slot(&slot) else {
continue;
};
if let Some(value) = get(core, scope, &slot).await? {
entries.insert(index, value);
}
}
if entries.is_empty() {
return Ok(None);
}
Ok(Some(IndexedHooks::from_entries(entries)))
}
pub(crate) async fn typed_all(
core: &Core,
scope: Scope<'_>,
) -> Result<BTreeMap<String, OptionValue>, Error> {
let mut decoded = BTreeMap::new();
for name in names(core, scope).await? {
if let Some(value) = get(core, scope, &name).await? {
let kind_name = split_slot(&name).map_or(name.as_str(), |(base, _)| base);
decoded.insert(name.clone(), OptionValue::decode(kind_name, value));
}
}
Ok(decoded)
}
pub(crate) async fn set_hooks(
core: &Core,
scope: Scope<'_>,
name: &str,
hooks: &IndexedHooks,
replace: ReplaceMode,
) -> Result<(), Error> {
ensure_scope(core, scope, name).await?;
let mut commands = Vec::with_capacity(hooks.len() + 1);
if replace == ReplaceMode::Replace {
commands.push(
scope
.apply(Command::new("set-hook"))
.arg("-u")
.arg("--")
.arg(OsString::from(name)),
);
}
for (index, value) in hooks {
commands.push(
scope
.apply(Command::new("set-hook"))
.arg("--")
.arg(OsString::from(format!("{name}[{index}]")))
.sensitive_arg(OsString::from_vec(value.as_bytes().to_vec())),
);
}
let mut commands = commands.into_iter();
let Some(first) = commands.next() else {
return Ok(());
};
let Some(second) = commands.next() else {
return run(core, "set-hook", None, first).await;
};
run(core, "set-hook", None, first).await?;
let result = match commands.next() {
None => core.execute(second).await,
Some(third) => {
let mut chain = CommandChain::new(second).then(third);
for command in commands {
chain = chain.then(command);
}
core.execute_chain(chain).await
}
};
let result = result.map_err(|error| error.after_effect("set-hooks"))?;
if result.success() {
return Ok(());
}
Err(mutation_failure("set-hook", None, &result).after_effect("set-hooks"))
}