rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::{
    command_parser::{CommandParser, ParsedCommand},
    formats::{FormatContext, FormatVariables},
};
use rmux_proto::RmuxError;

use super::super::prompt_support::{
    CommandPromptPlan, ConfirmBeforePlan, PromptField, PromptQueueResult, PromptStartOutcome,
};
use super::super::RequestHandler;
use super::command_args::CommandListArgument;
use super::format_context::{collect_parse_time_values, format_context_for_target};
use super::prompt_parse::{ParsedCommandPromptCommand, ParsedConfirmBeforeCommand};
use super::queue::{prompt_queue_action_from_result, QueueCommandAction, QueueExecutionContext};
use super::targets::active_session_target;
use crate::format_runtime::{render_runtime_template, RuntimeFormatContext};

impl RequestHandler {
    pub(super) async fn execute_queued_command_prompt(
        &self,
        requester_pid: u32,
        command: ParsedCommandPromptCommand,
        context: &QueueExecutionContext,
    ) -> Result<QueueCommandAction, RmuxError> {
        let session_candidate = if context.current_target.is_none() {
            self.current_session_candidate(requester_pid).await
        } else {
            None
        };
        let attached_count = match context.current_target.as_ref() {
            Some(target) => self.attached_count(target.session_name()).await,
            None => match session_candidate.as_ref() {
                Some(session_name) => self.attached_count(session_name).await,
                None => 0,
            },
        };
        let (template, fields, format_values) = {
            let state = self.state.lock().await;
            let format_target = context.current_target.clone().or_else(|| {
                session_candidate
                    .as_ref()
                    .and_then(|session_name| active_session_target(&state.sessions, session_name))
            });
            let runtime = match format_target.as_ref() {
                Some(target) => format_context_for_target(&state, target, attached_count)?,
                None => RuntimeFormatContext::new(FormatContext::new()).with_state(&state),
            };
            let template = match &command.template {
                Some(CommandListArgument::Parsed(commands)) => commands.to_tmux_string(),
                Some(CommandListArgument::String(value)) if command.format_template => {
                    render_runtime_template(value, &runtime, true)
                }
                Some(CommandListArgument::String(value)) => value.clone(),
                None => "%1".to_owned(),
            };
            let default_prompts = if command.template.is_some() {
                format!(
                    "({})",
                    command_prompt_default_label(command.template.as_ref(), &template)
                )
            } else {
                ":".to_owned()
            };
            let fields = if command.literal {
                vec![PromptField {
                    prompt: match &command.prompts {
                        Some(prompts) => render_runtime_template(prompts, &runtime, true),
                        None => default_prompts.clone(),
                    },
                    input: command
                        .inputs
                        .as_deref()
                        .map(|inputs| render_runtime_template(inputs, &runtime, true))
                        .unwrap_or_default(),
                }]
            } else {
                render_split_prompt_fields(
                    command.prompts.as_deref().unwrap_or(&default_prompts),
                    command.inputs.as_deref(),
                    command.prompts.is_some() || command.template.is_some(),
                    &runtime,
                )
            };

            (template, fields, collect_parse_time_values(&runtime))
        };

        let plan = CommandPromptPlan {
            requester_pid,
            target_client: command.target_client.clone(),
            context: context.clone(),
            fields,
            template,
            flags: command.flags,
            prompt_type: command.prompt_type,
            background: command.background,
            format_values,
        };
        let result = match self.start_command_prompt(plan).await? {
            PromptStartOutcome::Immediate => {
                return Ok(QueueCommandAction::Normal {
                    output: None,
                    error: None,
                });
            }
            PromptStartOutcome::Waiting(receiver) => {
                receiver.await.unwrap_or_else(|_| PromptQueueResult::noop())
            }
        };
        Ok(prompt_queue_action_from_result(result))
    }

    pub(super) async fn execute_queued_confirm_before(
        &self,
        requester_pid: u32,
        command: ParsedConfirmBeforeCommand,
        context: &QueueExecutionContext,
    ) -> Result<QueueCommandAction, RmuxError> {
        let session_candidate = if context.current_target.is_none() {
            self.current_session_candidate(requester_pid).await
        } else {
            None
        };
        let attached_count = match context.current_target.as_ref() {
            Some(target) => self.attached_count(target.session_name()).await,
            None => match session_candidate.as_ref() {
                Some(session_name) => self.attached_count(session_name).await,
                None => 0,
            },
        };
        let (prompt, template, format_values) = {
            let state = self.state.lock().await;
            let format_target = context.current_target.clone().or_else(|| {
                session_candidate
                    .as_ref()
                    .and_then(|session_name| active_session_target(&state.sessions, session_name))
            });
            let runtime = match format_target.as_ref() {
                Some(target) => format_context_for_target(&state, target, attached_count)?,
                None => RuntimeFormatContext::new(FormatContext::new()).with_state(&state),
            };
            let format_values = collect_parse_time_values(&runtime);
            let parsed = match &command.command {
                CommandListArgument::Parsed(commands) => commands.clone(),
                CommandListArgument::String(value) => {
                    let mut parser =
                        CommandParser::new().with_environment_store(&state.environment);
                    for (name, value) in &format_values {
                        parser = parser.with_format_value(name, value.clone());
                    }
                    parser
                        .parse_one_group(value)
                        .map_err(super::command_parse_error_to_rmux)?
                }
            };
            let template = parsed.to_tmux_string();
            let prompt = match &command.prompt {
                Some(prompt) => format!("{} ", render_runtime_template(prompt, &runtime, true)),
                None => {
                    let name = parsed
                        .commands()
                        .first()
                        .map(ParsedCommand::name)
                        .unwrap_or("");
                    format!("Confirm '{name}'? ({}/n) ", command.confirm_key)
                }
            };
            (prompt, template, format_values)
        };

        let plan = ConfirmBeforePlan {
            requester_pid,
            target_client: command.target_client.clone(),
            context: context.clone(),
            prompt,
            template,
            confirm_key: command.confirm_key,
            default_yes: command.default_yes,
            background: command.background,
            format_values,
        };
        let result = match self.start_confirm_before(plan).await? {
            PromptStartOutcome::Immediate => {
                return Ok(QueueCommandAction::Normal {
                    output: None,
                    error: None,
                });
            }
            PromptStartOutcome::Waiting(receiver) => {
                receiver.await.unwrap_or_else(|_| PromptQueueResult::noop())
            }
        };
        Ok(prompt_queue_action_from_result(result))
    }
}

fn command_prompt_default_label(template: Option<&CommandListArgument>, rendered: &str) -> String {
    match template {
        Some(CommandListArgument::Parsed(commands)) => commands
            .commands()
            .first()
            .map(|command| command.name().to_owned())
            .unwrap_or_else(|| rendered.to_owned()),
        Some(CommandListArgument::String(_)) => CommandParser::new()
            .parse_one_group(rendered)
            .ok()
            .and_then(|commands| {
                commands
                    .commands()
                    .first()
                    .map(|command| command.name().to_owned())
            })
            .unwrap_or_else(|| rendered.to_owned()),
        None => ":".to_owned(),
    }
}

fn render_split_prompt_fields<V>(
    prompts: &str,
    inputs: Option<&str>,
    append_space: bool,
    variables: &V,
) -> Vec<PromptField>
where
    V: FormatVariables + ?Sized,
{
    let mut input_parts = inputs.map(|value| value.split(','));
    prompts
        .split(',')
        .map(|prompt| {
            let mut prompt = render_runtime_template(prompt, variables, true);
            if append_space {
                prompt.push(' ');
            }
            PromptField {
                prompt,
                input: input_parts
                    .as_mut()
                    .and_then(|parts| parts.next())
                    .map(|input| render_runtime_template(input, variables, true))
                    .unwrap_or_default(),
            }
        })
        .collect()
}