rmux 0.7.1

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::collections::BTreeSet;

pub(super) fn normalize(command_name: &str, arguments: Vec<String>) -> Vec<String> {
    match command_name {
        "split-window" => normalize_split_window(arguments),
        "resize-pane" => normalize_resize_pane(arguments),
        _ => arguments,
    }
}

fn normalize_split_window(arguments: Vec<String>) -> Vec<String> {
    let arguments = collapse_short_flag_group_in_option_prefix(
        arguments,
        &BTreeSet::from(['h', 'v']),
        &BTreeSet::from(['c', 'e', 'F', 'l', 'p', 't']),
    );
    collapse_value_flag_group_in_option_prefix(
        arguments,
        &BTreeSet::from(['l', 'p']),
        &BTreeSet::from(['c', 'e', 'F', 'l', 'p', 't']),
    )
}

fn normalize_resize_pane(arguments: Vec<String>) -> Vec<String> {
    let occurrences = resize_adjustment_occurrences(&arguments);
    if occurrences.len() <= 1 {
        return arguments;
    }

    if occurrences
        .iter()
        .take(occurrences.len().saturating_sub(1))
        .any(|occurrence| occurrence.has_explicit_relative_value)
    {
        return arguments;
    }

    drop_occurrences_except_last(arguments, occurrences)
}

#[derive(Clone)]
struct OptionOccurrence {
    indexes: Vec<usize>,
    has_explicit_relative_value: bool,
    kind: OptionOccurrenceKind,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum OptionOccurrenceKind {
    AbsoluteResize,
    Other,
}

fn resize_adjustment_occurrences(arguments: &[String]) -> Vec<OptionOccurrence> {
    let mut occurrences = Vec::new();
    let mut index = 0;
    while index < arguments.len() {
        match arguments[index].as_str() {
            "--" => break,
            "-D" | "-U" | "-L" | "-R" => {
                let explicit_value = arguments
                    .get(index + 1)
                    .is_some_and(|value| !value.starts_with('-'));
                let indexes = if explicit_value {
                    vec![index, index + 1]
                } else {
                    vec![index]
                };
                occurrences.push(OptionOccurrence {
                    indexes,
                    has_explicit_relative_value: explicit_value,
                    kind: OptionOccurrenceKind::Other,
                });
                index += if explicit_value { 2 } else { 1 };
            }
            "-x" | "-y" => {
                let mut indexes = vec![index];
                if index + 1 < arguments.len() {
                    indexes.push(index + 1);
                    index += 2;
                } else {
                    index += 1;
                }
                if let Some(last) = occurrences
                    .last_mut()
                    .filter(|occurrence| occurrence.kind == OptionOccurrenceKind::AbsoluteResize)
                {
                    last.indexes.extend(indexes);
                } else {
                    occurrences.push(OptionOccurrence {
                        indexes,
                        has_explicit_relative_value: false,
                        kind: OptionOccurrenceKind::AbsoluteResize,
                    });
                }
            }
            "-Z" => {
                occurrences.push(OptionOccurrence {
                    indexes: vec![index],
                    has_explicit_relative_value: false,
                    kind: OptionOccurrenceKind::Other,
                });
                index += 1;
            }
            _ => index += 1,
        }
    }
    occurrences
}

fn collapse_short_flag_group_in_option_prefix(
    arguments: Vec<String>,
    group_flags: &BTreeSet<char>,
    value_flags: &BTreeSet<char>,
) -> Vec<String> {
    let Some(last) = last_short_flag_group_occurrence(&arguments, group_flags, value_flags) else {
        return arguments;
    };

    let mut occurrence = 0usize;
    let mut output = Vec::with_capacity(arguments.len());
    let mut index = 0;
    while index < arguments.len() {
        let argument = &arguments[index];
        if argument == "--" {
            output.extend(arguments[index..].iter().cloned());
            break;
        }
        if is_trailing_position_start(argument) {
            output.extend(arguments[index..].iter().cloned());
            break;
        }
        if let Some(stripped) = short_flag_cluster(argument) {
            let mut rewritten = String::from("-");
            let mut consumes_value = false;
            let mut emitted_value_flag = false;
            let mut chars = stripped.char_indices().peekable();
            while let Some((_, flag)) = chars.next() {
                let value_start = chars.peek().map_or(stripped.len(), |(index, _)| *index);
                let keep = if group_flags.contains(&flag) {
                    let keep = occurrence == last;
                    occurrence += 1;
                    keep
                } else {
                    true
                };

                if value_flags.contains(&flag) {
                    if keep {
                        if rewritten.len() > 1 {
                            output.push(rewritten);
                            rewritten = String::from("-");
                        }
                        output.push(format!("-{flag}"));
                    }
                    emitted_value_flag = keep;
                    let attached_value = &stripped[value_start..];
                    if attached_value.is_empty() {
                        consumes_value = true;
                    } else if keep {
                        output.push(attached_value.to_owned());
                    }
                    break;
                }

                if keep {
                    rewritten.push(flag);
                }
            }
            if !emitted_value_flag && rewritten.len() > 1 {
                output.push(rewritten);
            }
            if consumes_value && index + 1 < arguments.len() {
                index += 1;
                output.push(arguments[index].clone());
            }
        } else {
            output.push(argument.clone());
        }
        index += 1;
    }
    output
}

fn last_short_flag_group_occurrence(
    arguments: &[String],
    group_flags: &BTreeSet<char>,
    value_flags: &BTreeSet<char>,
) -> Option<usize> {
    let mut last = None;
    let mut occurrence = 0usize;
    let mut index = 0;
    while index < arguments.len() {
        let argument = &arguments[index];
        if argument == "--" || is_trailing_position_start(argument) {
            break;
        }
        if let Some(stripped) = short_flag_cluster(argument) {
            let mut chars = stripped.char_indices().peekable();
            while let Some((_, flag)) = chars.next() {
                let value_start = chars.peek().map_or(stripped.len(), |(index, _)| *index);
                if group_flags.contains(&flag) {
                    last = Some(occurrence);
                    occurrence += 1;
                }
                if value_flags.contains(&flag) {
                    if stripped[value_start..].is_empty() {
                        index += 1;
                    }
                    break;
                }
            }
        }
        index += 1;
    }
    last
}

fn collapse_value_flag_group_in_option_prefix(
    arguments: Vec<String>,
    group_flags: &BTreeSet<char>,
    value_flags: &BTreeSet<char>,
) -> Vec<String> {
    let occurrences = value_flag_group_occurrences(&arguments, group_flags, value_flags);
    if occurrences.len() <= 1 {
        return arguments;
    }
    drop_occurrences_except_last(arguments, occurrences)
}

fn value_flag_group_occurrences(
    arguments: &[String],
    group_flags: &BTreeSet<char>,
    value_flags: &BTreeSet<char>,
) -> Vec<OptionOccurrence> {
    let mut occurrences = Vec::new();
    let mut index = 0;
    while index < arguments.len() {
        let argument = &arguments[index];
        if argument == "--" || is_trailing_position_start(argument) {
            break;
        }
        if let Some(flag) = exact_short_flag(argument, value_flags) {
            let has_value = index + 1 < arguments.len();
            if group_flags.contains(&flag) {
                let indexes = if has_value {
                    vec![index, index + 1]
                } else {
                    vec![index]
                };
                occurrences.push(OptionOccurrence {
                    indexes,
                    has_explicit_relative_value: false,
                    kind: OptionOccurrenceKind::Other,
                });
            }
            index += if has_value { 2 } else { 1 };
        } else {
            index += 1;
        }
    }
    occurrences
}

fn drop_occurrences_except_last(
    arguments: Vec<String>,
    occurrences: Vec<OptionOccurrence>,
) -> Vec<String> {
    let mut drop_indexes = BTreeSet::new();
    for occurrence in occurrences.iter().take(occurrences.len().saturating_sub(1)) {
        drop_indexes.extend(occurrence.indexes.iter().copied());
    }

    arguments
        .into_iter()
        .enumerate()
        .filter_map(|(index, argument)| (!drop_indexes.contains(&index)).then_some(argument))
        .collect()
}

fn short_flag_cluster(argument: &str) -> Option<&str> {
    let stripped = argument.strip_prefix('-')?;
    (!stripped.is_empty() && !stripped.starts_with('-')).then_some(stripped)
}

fn is_trailing_position_start(argument: &str) -> bool {
    !argument.starts_with('-') || argument == "-"
}

fn exact_short_flag(argument: &str, flags: &BTreeSet<char>) -> Option<char> {
    let mut chars = argument.chars();
    if chars.next()? != '-' || chars.as_str().starts_with('-') {
        return None;
    }
    let flag = chars.next()?;
    (chars.next().is_none() && flags.contains(&flag)).then_some(flag)
}