rtx-cli 2024.0.0

Polyglot runtime manager (asdf rust clone)
use clap::builder::StyledStr;
use clap::{Arg, Command, ValueHint};
use itertools::Itertools;

use crate::shell::completions::is_banned;

pub fn render(cmd: &Command) -> String {
    let cmds = vec![cmd];
    let subcommands = render_subcommands(&cmds).join("\n");

    format! {r#"
set -l fssf "__fish_seen_subcommand_from"

{subcommands}

function __rtx_all_plugins
    if test -z "$__rtx_all_plugins_cache"
        set -g __rtx_all_plugins_cache (rtx plugins ls --all)
    end
    for p in $__rtx_all_plugins_cache
        echo $p
    end
end
function __rtx_plugins
    if test -z "$__rtx_plugins_cache"
        set -g __rtx_plugins_cache (rtx plugins ls --core --user)
    end
    for p in $__rtx_plugins_cache
        echo $p
    end
end
function __rtx_tool_versions
    if test -z "$__rtx_tool_versions_cache"
        set -g __rtx_tool_versions_cache (rtx plugins --core --user) (rtx ls-remote --all | tac)
    end
    for tv in $__rtx_tool_versions_cache
        echo $tv
    end
end
function __rtx_installed_tool_versions
    for tv in (rtx ls --installed | awk '{{print $1 "@" $2}}')
        echo $tv
    end
end
function __rtx_aliases
    if test -z "$__rtx_aliases_cache"
        set -g __rtx_aliases_cache (rtx alias ls | awk '{{print $2}}')
    end
    for a in $__rtx_aliases_cache
        echo $a
    end
end
function __rtx_tasks
    for tv in (rtx task ls --no-header | awk '{{print $1}}')
        echo $tv
    end
end
function __rtx_settings
    if test -z "$__rtx_settings_cache"
        set -g __rtx_settings_cache (rtx settings ls | awk '{{print $1}}')
    end
    for s in $__rtx_settings_cache
        echo $s
    end
end

# vim: noet ci pi sts=0 sw=4 ts=4
"#}
}

fn render_args(cmds: &[&Command]) -> Vec<String> {
    let cmd = cmds[cmds.len() - 1];
    cmd.get_arguments()
        .filter(|a| !a.is_hide_set())
        .sorted_by_cached_key(|a| a.get_id())
        .map(|a| render_arg(cmds, a))
        .collect()
}

fn render_arg(cmds: &[&Command], a: &Arg) -> String {
    let mut complete_cmd = r#"complete -kxc rtx"#.to_string();
    let parents = cmds.iter().skip(1).map(|c| c.get_name()).collect_vec();
    if cmds.len() > 1 {
        let mut p = format!("$fssf {}", &parents[0]);
        for parent in &parents[1..] {
            p.push_str(&format!("; and $fssf {}", parent));
        }
        complete_cmd.push_str(&format!(r#" -n "{p}""#));
    }
    if let Some(short) = a.get_short() {
        complete_cmd.push_str(&format!(" -s {}", short));
    }
    if let Some(long) = a.get_long() {
        complete_cmd.push_str(&format!(" -l {}", long));
    }
    if let Some(c) = render_completer(a) {
        complete_cmd.push_str(&format!(r#" -a "{c}""#));
    }
    let help = about_to_help(a.get_help());
    complete_cmd.push_str(&format!(" -d '{help}'"));
    complete_cmd
}

fn render_completer(a: &Arg) -> Option<String> {
    let possible_values = a.get_possible_values();
    if !possible_values.is_empty() {
        return Some(
            possible_values
                .iter()
                .map(|v| escape_value(v.get_name()))
                .collect::<Vec<_>>()
                .join(" "),
        );
    }
    match a.get_value_hint() {
        ValueHint::DirPath => Some("(__fish_complete_directories)".to_string()),
        ValueHint::FilePath => Some("(__fish_complete_path)".to_string()),
        ValueHint::AnyPath => Some("(__fish_complete_path)".to_string()),
        _ => match a.get_id().as_str() {
            "tool" => Some("(__rtx_tool_versions)".to_string()),
            "installed_tool" => Some("(__rtx_installed_tool_versions)".to_string()),
            "plugin" => Some("(__rtx_plugins)".to_string()),
            "new_plugin" => Some("(__rtx_all_plugins)".to_string()),
            "alias" => Some("(__rtx_aliases)".to_string()),
            "setting" => Some("(__rtx_settings)".to_string()),
            "task" => Some("(__rtx_tasks)".to_string()),
            //"prefix" => Some("(__rtx_prefixes)".to_string()),
            _ => None,
        },
    }
}

fn render_subcommands(cmds: &[&Command]) -> Vec<String> {
    let cmd = cmds[cmds.len() - 1];
    let full_name = cmds.iter().skip(1).map(|c| c.get_name()).join(" ");
    let parents = cmds.iter().skip(1).map(|c| c.get_name()).collect_vec();

    let args = render_args(cmds);
    let subcommands = cmd
        .get_subcommands()
        .filter(|c| !is_banned(c) && !c.is_hide_set())
        .sorted_by_cached_key(|c| c.get_name())
        .collect_vec();
    let command_names = subcommands.iter().map(|c| c.get_name()).join(" ");

    let subcommand_defs = subcommands.iter().map(|cmd| {
        let mut cmds = cmds.iter().copied().collect_vec();
        cmds.push(cmd);
        let name = cmd.get_name();
        let help = about_to_help(cmd.get_about());
        if parents.is_empty() {
            format!(r#"complete -xc rtx -n "not $fssf $others" -a {name} -d '{help}'"#)
        } else {
            let mut p = format!("$fssf {}", &parents[0]);
            for parent in &parents[1..] {
                p.push_str(&format!("; and $fssf {}", parent));
            }
            format!(r#"complete -xc rtx -n "{p}; and not $fssf $others" -a {name} -d '{help}'"#)
        }
    });

    let rendered_subcommands = subcommands.iter().flat_map(|cmd| {
        let mut cmds = cmds.iter().copied().collect_vec();
        cmds.push(cmd);
        render_subcommands(&cmds)
    });

    let mut out = vec![format! {"# {}", if full_name.is_empty() { "rtx" } else { &full_name }}];
    out.extend(args);
    if !subcommands.is_empty() {
        out.push(format! {"set -l others {command_names}"});
        out.extend(subcommand_defs);
        out.push(String::new());
        out.extend(rendered_subcommands);
    }
    out.push(String::new());
    out
}

fn first_line(s: &str) -> &str {
    s.lines().next().unwrap_or_default()
}

fn help_escape(s: &str) -> String {
    s.replace('\\', "\\\\").replace('\'', "'\\''")
}

fn about_to_help(ss: Option<&StyledStr>) -> String {
    match ss {
        Some(ss) => help_escape(first_line(&ss.to_string())),
        None => String::new(),
    }
}

fn escape_value(string: &str) -> String {
    string
        .replace('\\', "\\\\")
        .replace('\'', "'\\''")
        .replace('[', "\\[")
        .replace(']', "\\]")
        .replace(':', "\\:")
        .replace('$', "\\$")
        .replace('`', "\\`")
        .replace('(', "\\(")
        .replace(')', "\\)")
        .replace(' ', "\\ ")
}