rmux 0.9.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use rmux_client::Connection;
use rmux_proto::types::OptionScopeSelector;
use rmux_proto::{PaneTarget, ResolveTargetType, RmuxError, SessionName, Target, WindowTarget};

use crate::cli::ExitFailure;
use crate::cli_args::{ShowOptionsArgs, ShowOptionsCommandKind, TargetSpec};

use super::super::super::{
    resolve_current_pane_target, resolve_current_session_target, resolve_target_spec,
    resolve_window_target_or_current,
};

pub(in crate::cli::config_commands) fn resolve_show_options_scope(
    command: ShowOptionsCommandKind,
    args: &ShowOptionsArgs,
) -> Result<ShowOptionsScope, ExitFailure> {
    let force_window = matches!(command, ShowOptionsCommandKind::ShowWindowOptions);
    let hook = args.name.as_deref().and_then(show_options_hook_name);
    if args.server {
        if let Some(name) = args.name.as_deref() {
            if hook.is_none()
                && !option_supports_show_scope(name, &OptionScopeSelector::ServerGlobal)
            {
                return show_named_scope_fallback(args.target.as_ref(), name);
            }
        }
        return Ok(OptionScopeSelector::ServerGlobal.into());
    }

    match (args.window || force_window, args.pane, args.target.as_ref()) {
        (true, false, _) if args.global => Ok(OptionScopeSelector::WindowGlobal.into()),
        (true, false, Some(target)) => {
            if let Some(name) = args.name.as_deref() {
                if hook.is_none()
                    && !option_supports_show_scope(name, &OptionScopeSelector::WindowGlobal)
                {
                    return show_options_scope_for_target(target, Some(name));
                }
            }
            Ok(ShowOptionsScope::Unresolved {
                target: target.clone(),
                kind: UnresolvedShowOptionsScope::Window,
            })
        }
        (true, false, None) => {
            if let Some(name) = args.name.as_deref() {
                if hook.is_none()
                    && !option_supports_show_scope(name, &OptionScopeSelector::WindowGlobal)
                {
                    return show_named_scope_fallback(None, name);
                }
            }
            Ok(ShowOptionsScope::CurrentWindow)
        }
        (false, true, _) if args.global && hook.is_some() => show_pane_scope(args.target.as_ref()),
        (false, true, _) if args.global => show_global_pane_options_scope(args),
        (false, true, Some(target)) => {
            if let Some(name) = args.name.as_deref() {
                if hook.is_none() && !option_supports_show_scope(name, &dummy_pane_scope()) {
                    return show_options_scope_for_target(target, Some(name));
                }
            }
            Ok(ShowOptionsScope::Unresolved {
                target: target.clone(),
                kind: UnresolvedShowOptionsScope::Pane,
            })
        }
        (false, true, None) => {
            if let Some(name) = args.name.as_deref() {
                if hook.is_none() && !option_supports_show_scope(name, &dummy_pane_scope()) {
                    return show_named_scope_fallback(None, name);
                }
            }
            Ok(ShowOptionsScope::CurrentPane)
        }
        (false, false, _) if args.global => Ok(if let Some(hook) = hook {
            global_hook_option_scope(hook)
        } else if let Some(name) = args.name.as_deref() {
            rmux_core::default_global_scope_for_option_name(name)
                .map_err(option_lookup_exit_failure)?
        } else if force_window {
            OptionScopeSelector::WindowGlobal
        } else {
            OptionScopeSelector::SessionGlobal
        }
        .into()),
        (false, false, Some(target)) if hook.is_some() => Ok(ShowOptionsScope::Unresolved {
            target: target.clone(),
            kind: hook_scope_kind(hook.expect("hook checked above")),
        }),
        (false, false, Some(target)) => show_options_scope_for_target(target, args.name.as_deref()),
        (false, false, None) if force_window => Ok(ShowOptionsScope::CurrentWindow),
        (false, false, None) if hook.is_some() => {
            Ok(current_hook_scope(hook.expect("hook checked above")))
        }
        (false, false, None) => Ok(ShowOptionsScope::CurrentSession),
        (true, true, _) => unreachable!("clap scope group prevents -w and -p together"),
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::cli::config_commands) enum ShowOptionsScope {
    Resolved(OptionScopeSelector),
    CurrentSession,
    CurrentWindow,
    CurrentPane,
    Unresolved {
        target: TargetSpec,
        kind: UnresolvedShowOptionsScope,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::cli::config_commands) enum UnresolvedShowOptionsScope {
    Session,
    Window,
    Pane,
}

impl ShowOptionsScope {
    pub(in crate::cli::config_commands) fn resolve(
        self,
        connection: &mut Connection,
        command_name: &str,
    ) -> Result<OptionScopeSelector, ExitFailure> {
        match self {
            Self::Resolved(scope) => Ok(scope),
            Self::CurrentSession => {
                resolve_current_session_target(connection).map(OptionScopeSelector::Session)
            }
            Self::CurrentWindow => resolve_window_target_or_current(connection, None, command_name)
                .map(OptionScopeSelector::Window),
            Self::CurrentPane => {
                resolve_current_pane_target(connection, command_name).map(OptionScopeSelector::Pane)
            }
            Self::Unresolved { target, kind } => {
                resolve_unresolved_show_options_scope(connection, &target, kind)
            }
        }
    }
}

impl From<OptionScopeSelector> for ShowOptionsScope {
    fn from(scope: OptionScopeSelector) -> Self {
        Self::Resolved(scope)
    }
}

fn show_global_pane_options_scope(args: &ShowOptionsArgs) -> Result<ShowOptionsScope, ExitFailure> {
    let pane_scope = dummy_pane_scope();
    if let Some(name) = args.name.as_deref() {
        match rmux_core::resolve_option_name(name) {
            Ok(query) if query.is_user() || query.supports_scope(&pane_scope) => {
                return show_pane_scope(args.target.as_ref());
            }
            Ok(_) => {
                return Ok(rmux_core::default_global_scope_for_option_name(name)
                    .map_err(option_lookup_exit_failure)?
                    .into());
            }
            Err(error) => return Err(option_lookup_exit_failure(error)),
        }
    }

    show_pane_scope(args.target.as_ref())
}

fn show_pane_scope(target: Option<&TargetSpec>) -> Result<ShowOptionsScope, ExitFailure> {
    match target {
        Some(target) => Ok(ShowOptionsScope::Unresolved {
            target: target.clone(),
            kind: UnresolvedShowOptionsScope::Pane,
        }),
        None => Ok(ShowOptionsScope::CurrentPane),
    }
}

fn resolve_unresolved_show_options_scope(
    connection: &mut Connection,
    target: &TargetSpec,
    kind: UnresolvedShowOptionsScope,
) -> Result<OptionScopeSelector, ExitFailure> {
    let target_type = match kind {
        UnresolvedShowOptionsScope::Session => ResolveTargetType::Session,
        UnresolvedShowOptionsScope::Window => ResolveTargetType::Window,
        UnresolvedShowOptionsScope::Pane => ResolveTargetType::Pane,
    };
    let target = resolve_target_spec(connection, target, target_type, false, false)?;
    match (kind, target) {
        (UnresolvedShowOptionsScope::Pane, Target::Pane(target)) => {
            Ok(OptionScopeSelector::Pane(target))
        }
        (UnresolvedShowOptionsScope::Pane, _) => Err(ExitFailure::new(
            1,
            "show-options -p requires a pane target",
        )),
        (UnresolvedShowOptionsScope::Session, Target::Session(session_name)) => {
            Ok(OptionScopeSelector::Session(session_name))
        }
        (UnresolvedShowOptionsScope::Session, Target::Window(target)) => {
            Ok(OptionScopeSelector::Session(target.session_name().clone()))
        }
        (UnresolvedShowOptionsScope::Session, Target::Pane(target)) => {
            Ok(OptionScopeSelector::Session(target.session_name().clone()))
        }
        (UnresolvedShowOptionsScope::Window, Target::Session(session_name)) => {
            Ok(OptionScopeSelector::Window(WindowTarget::new(session_name)))
        }
        (UnresolvedShowOptionsScope::Window, Target::Window(target)) => {
            Ok(OptionScopeSelector::Window(target))
        }
        (UnresolvedShowOptionsScope::Window, Target::Pane(target)) => {
            Ok(OptionScopeSelector::Window(WindowTarget::with_window(
                target.session_name().clone(),
                target.window_index(),
            )))
        }
    }
}

fn show_options_scope_for_target(
    target: &TargetSpec,
    name: Option<&str>,
) -> Result<ShowOptionsScope, ExitFailure> {
    let Some(name) = name else {
        return Ok(ShowOptionsScope::Unresolved {
            target: target.clone(),
            kind: UnresolvedShowOptionsScope::Session,
        });
    };

    match rmux_core::default_global_scope_for_option_name(name)
        .map_err(option_lookup_exit_failure)?
    {
        OptionScopeSelector::ServerGlobal => Ok(OptionScopeSelector::ServerGlobal.into()),
        OptionScopeSelector::WindowGlobal | OptionScopeSelector::Window(_) => {
            Ok(ShowOptionsScope::Unresolved {
                target: target.clone(),
                kind: UnresolvedShowOptionsScope::Window,
            })
        }
        OptionScopeSelector::Pane(_) => Ok(ShowOptionsScope::Unresolved {
            target: target.clone(),
            kind: UnresolvedShowOptionsScope::Pane,
        }),
        OptionScopeSelector::SessionGlobal | OptionScopeSelector::Session(_) => {
            Ok(ShowOptionsScope::Unresolved {
                target: target.clone(),
                kind: UnresolvedShowOptionsScope::Session,
            })
        }
    }
}

fn show_named_scope_fallback(
    target: Option<&TargetSpec>,
    name: &str,
) -> Result<ShowOptionsScope, ExitFailure> {
    if let Some(target) = target {
        return show_options_scope_for_target(target, Some(name));
    }

    match rmux_core::default_global_scope_for_option_name(name)
        .map_err(option_lookup_exit_failure)?
    {
        OptionScopeSelector::ServerGlobal => Ok(OptionScopeSelector::ServerGlobal.into()),
        OptionScopeSelector::WindowGlobal | OptionScopeSelector::Window(_) => {
            Ok(ShowOptionsScope::CurrentWindow)
        }
        OptionScopeSelector::Pane(_) => Ok(ShowOptionsScope::CurrentPane),
        OptionScopeSelector::SessionGlobal | OptionScopeSelector::Session(_) => {
            Ok(ShowOptionsScope::CurrentSession)
        }
    }
}

fn option_supports_show_scope(name: &str, scope: &OptionScopeSelector) -> bool {
    rmux_core::resolve_option_name(name)
        .map(|query| query.supports_scope(scope))
        .unwrap_or(false)
}

fn dummy_pane_scope() -> OptionScopeSelector {
    OptionScopeSelector::Pane(PaneTarget::with_window(
        SessionName::new("show-scope").expect("valid session name"),
        0,
        0,
    ))
}

fn show_options_hook_name(value: &str) -> Option<rmux_proto::HookName> {
    let name = match value.rsplit_once('[') {
        Some((name, index)) if index.strip_suffix(']')?.parse::<u32>().is_ok() => name,
        Some(_) => return None,
        None => value,
    };
    rmux_proto::HookName::from_str(name)
}

fn global_hook_option_scope(hook: rmux_proto::HookName) -> OptionScopeSelector {
    match rmux_core::hook_global_root(hook) {
        rmux_core::HookGlobalRoot::Session => OptionScopeSelector::SessionGlobal,
        rmux_core::HookGlobalRoot::Window => OptionScopeSelector::WindowGlobal,
    }
}

fn hook_scope_kind(hook: rmux_proto::HookName) -> UnresolvedShowOptionsScope {
    let target = Target::Pane(PaneTarget::with_window(
        SessionName::new("show-hook-scope").expect("valid session name"),
        0,
        0,
    ));
    match rmux_core::hook_natural_scope_for_target(hook, target) {
        rmux_proto::ScopeSelector::Session(_) => UnresolvedShowOptionsScope::Session,
        rmux_proto::ScopeSelector::Window(_) => UnresolvedShowOptionsScope::Window,
        rmux_proto::ScopeSelector::Pane(_) => UnresolvedShowOptionsScope::Pane,
        rmux_proto::ScopeSelector::Global => unreachable!("natural hook scope is local"),
    }
}

fn current_hook_scope(hook: rmux_proto::HookName) -> ShowOptionsScope {
    match hook_scope_kind(hook) {
        UnresolvedShowOptionsScope::Session => ShowOptionsScope::CurrentSession,
        UnresolvedShowOptionsScope::Window => ShowOptionsScope::CurrentWindow,
        UnresolvedShowOptionsScope::Pane => ShowOptionsScope::CurrentPane,
    }
}

fn option_lookup_exit_failure(error: RmuxError) -> ExitFailure {
    match error {
        RmuxError::Server(message) | RmuxError::Message(message) => {
            let normalized = message.strip_prefix("server error: ").unwrap_or(&message);
            ExitFailure::new(1, normalized.to_owned())
        }
        error => ExitFailure::new(1, error.to_string()),
    }
}