use std::collections::BTreeMap;
use std::ffi::OsString;
use crate::formats::TmuxText;
use crate::hooks::IndexedHooks;
use crate::hooks::ReplaceMode;
use crate::internal::core::Core;
use crate::options::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 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(OsString::from(name));
let result = core.execute(command).await?;
if !result.success() {
if name.starts_with('@') {
return Ok(None);
}
return Err(Error::refused(
"show-options",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
));
}
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::refused(
"show-options",
result.exit_code(),
result.stderr_lossy().into_owned(),
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");
}
run(
core,
command
.arg(OsString::from(name))
.sensitive_arg(value.into()),
)
.await
}
pub(crate) async fn unset(core: &Core, scope: Scope<'_>, name: &str) -> Result<(), Error> {
run(
core,
scope
.apply(Command::new("set-option"))
.arg("-u")
.arg(OsString::from(name)),
)
.await
}
pub(crate) async fn set_hook(
core: &Core,
scope: Scope<'_>,
name: &str,
command_text: impl Into<OsString>,
) -> Result<(), Error> {
run(
core,
scope
.apply(Command::new("set-hook"))
.arg(OsString::from(name))
.sensitive_arg(command_text.into()),
)
.await
}
pub(crate) async fn unset_hook(core: &Core, scope: Scope<'_>, name: &str) -> Result<(), Error> {
run(
core,
scope
.apply(Command::new("set-hook"))
.arg("-u")
.arg(OsString::from(name)),
)
.await
}
async fn run(core: &Core, command: Command) -> Result<(), Error> {
let result = core.execute(command).await?;
if result.success() {
return Ok(());
}
Err(Error::refused(
"set-option",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
))
}
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::refused(
"show-hooks",
result.exit_code(),
result.stderr_lossy().into_owned(),
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(OsString::from(name)),
)
.await?;
if !result.success() {
return Err(Error::refused(
"show-options",
result.exit_code(),
result.stderr_lossy().into_owned(),
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> {
let mut commands = Vec::with_capacity(hooks.len() + 1);
if replace == ReplaceMode::Replace {
commands.push(
scope
.apply(Command::new("set-hook"))
.arg("-u")
.arg(OsString::from(name)),
);
}
for (index, value) in hooks {
commands.push(
scope
.apply(Command::new("set-hook"))
.arg(OsString::from(format!("{name}[{index}]")))
.sensitive_arg(OsString::from(
String::from_utf8_lossy(value.as_bytes()).into_owned(),
)),
);
}
let mut commands = commands.into_iter();
let Some(first) = commands.next() else {
return Ok(());
};
let result = match commands.next() {
None => core.execute(first).await?,
Some(second) => {
let mut chain = CommandChain::new(first).then(second);
for command in commands {
chain = chain.then(command);
}
core.execute_chain(chain).await?
}
};
if result.success() {
return Ok(());
}
Err(Error::refused(
"set-hook",
result.exit_code(),
result.stderr_lossy().into_owned(),
None,
))
}