r2x 0.0.29

A framework plugin manager for the r2x power systems modeling ecosystem.
Documentation
use crate::commands::plugins::context::PluginContext;
use crate::common::GlobalOpts;
use crate::plugins::error::PluginError;
use crate::plugins::install::get_package_info;
use colored::Colorize;
use r2x_manifest::types::{Manifest, Package, Plugin};
use std::collections::BTreeMap;

/// Format the source origin for display: `pypi`, `file:///path`, or `ssh://...`
fn format_source(pkg: &Package) -> String {
    if pkg.editable_install {
        pkg.source_uri
            .as_ref()
            .map_or_else(|| "local".to_string(), |uri| format!("file://{}", uri))
    } else if let Some(ref uri) = pkg.source_uri {
        uri.strip_prefix("git+").unwrap_or(uri).to_string()
    } else {
        "pypi".to_string()
    }
}

pub fn list_plugins(
    opts: &GlobalOpts,
    plugin_filter: Option<String>,
    module_filter: Option<String>,
    ctx: &PluginContext,
) -> Result<(), PluginError> {
    let manifest = &ctx.manifest;

    let has_plugins = !manifest.is_empty();

    if !has_plugins {
        println!("There are no current plugins installed.\n");
        println!(
            "To install a plugin, run:\n  {} install <package>",
            "r2x".bold().cyan()
        );
        return Ok(());
    }

    // If a plugin filter is provided, show detailed information
    if let Some(ref plugin_name) = plugin_filter {
        return show_plugin_details(
            manifest,
            plugin_name,
            module_filter.as_deref(),
            opts.verbose,
            ctx,
        );
    }

    // Otherwise, show the standard list view
    let mut packages: BTreeMap<String, Vec<String>> = BTreeMap::new();
    for pkg in &manifest.packages {
        if pkg.plugins.is_empty() {
            continue;
        }
        let mut names: Vec<String> = pkg.plugins.iter().map(|p| p.name.to_string()).collect();
        names.sort();
        packages.insert(pkg.name.to_string(), names);
    }

    if has_plugins {
        println!("{}", "Plugins:".bold().green());

        // Get package version info
        let python_path = &ctx.python_path;
        let uv_path = &ctx.uv_path;

        for (package_name, plugin_names) in &packages {
            // Get package metadata
            let pkg = manifest
                .packages
                .iter()
                .find(|p| p.name.as_ref() == package_name);

            // Get version info
            let version_info = get_package_info(uv_path, python_path, package_name)
                .ok()
                .and_then(|(v, _)| v);

            let source = pkg.map_or_else(|| "pypi".to_string(), format_source);

            let version_str = version_info
                .as_ref()
                .map(|v| format!(" (v{})", v))
                .unwrap_or_default();

            println!(
                "  {}{}{}",
                format!("{}:", source).dimmed(),
                package_name.bold().blue(),
                version_str.dimmed()
            );

            for plugin_name in plugin_names {
                println!("    - {}", plugin_name);
            }
            println!();
        }

        println!("{}: {}", "Total plugin packages".bold(), packages.len());
    }

    Ok(())
}

fn show_plugin_details(
    manifest: &Manifest,
    plugin_filter: &str,
    module_filter: Option<&str>,
    verbose_level: u8,
    ctx: &PluginContext,
) -> Result<(), PluginError> {
    // Find the package containing this plugin
    let package = manifest
        .packages
        .iter()
        .find(|pkg| pkg.name.as_ref() == plugin_filter)
        .ok_or_else(|| {
            PluginError::InvalidArgs(format!("Plugin package '{}' not found", plugin_filter))
        })?;

    // Build package header with version and editable info
    let version_info = get_package_info(&ctx.uv_path, &ctx.python_path, package.name.as_ref())
        .ok()
        .and_then(|(v, _)| v);

    let source = format_source(package);

    let version_str = version_info
        .as_ref()
        .map(|v| format!(" (v{})", v))
        .unwrap_or_default();

    println!(
        "{} {}{}{}",
        "Package:".bold().green(),
        format!("{}:", source).dimmed(),
        package.name.as_ref().bold().blue(),
        version_str.dimmed()
    );
    println!();

    // Filter plugins by module name if provided
    let plugins_to_show: Vec<_> = if let Some(module_name) = module_filter {
        package
            .plugins
            .iter()
            .filter(|p| {
                // Match if the plugin name ends with the module filter
                // e.g., "r2x_reeds.break_gens" matches module "break_gens"
                let name_str = p.name.as_ref();
                let parts: Vec<&str> = name_str.split('.').collect();
                parts.last().is_some_and(|&last| last == module_name)
            })
            .collect()
    } else {
        package.plugins.iter().collect()
    };

    if plugins_to_show.is_empty() {
        return Err(PluginError::InvalidArgs(format!(
            "No plugins found matching the filter criteria in package '{}'",
            plugin_filter
        )));
    }

    for plugin in plugins_to_show {
        if verbose_level > 0 {
            show_plugin_verbose(plugin);
        } else {
            show_plugin_compact(plugin);
        }
        println!();
    }

    Ok(())
}

fn show_plugin_compact(plugin: &Plugin) {
    println!(
        "{} [{:?}]",
        plugin.name.as_ref().bold().cyan(),
        plugin.plugin_type
    );

    // Show module info
    println!("  {}: {}", "Module".dimmed(), plugin.module);

    // Show class or function name
    if let Some(ref class_name) = plugin.class_name {
        println!("  {}: {}", "Class".dimmed(), class_name);
    }
    if let Some(ref function_name) = plugin.function_name {
        println!("  {}: {}", "Function".dimmed(), function_name);
    }

    // Show config if available
    if let Some(ref config_class) = plugin.config_class {
        println!("  {}: {}", "Config".dimmed(), config_class);
    }

    // Show arguments if available
    if !plugin.parameters.is_empty() {
        println!("  {}:", "Arguments".dimmed());
        for param in &plugin.parameters {
            let req_marker = if param.required { "*" } else { " " };
            let default_str = param
                .default
                .as_ref()
                .map(|d| format!(" = {}", d))
                .unwrap_or_default();
            println!(
                "    {}{}: {}{}",
                req_marker,
                param.name,
                param.format_types(),
                default_str
            );

            if let Some(ref desc) = param.description {
                println!("      {}", desc.dimmed());
            }
        }
    }
}

fn show_plugin_verbose(plugin: &Plugin) {
    println!("{}", plugin.name.as_ref().bold().cyan());

    println!("  {}: {:?}", "Type".dimmed(), plugin.plugin_type);
    println!("  {}: {}", "Module".dimmed(), plugin.module);

    // Show class or function name
    if let Some(ref class_name) = plugin.class_name {
        println!("  {}: {}", "Class".dimmed(), class_name);
    }
    if let Some(ref function_name) = plugin.function_name {
        println!("  {}: {}", "Function".dimmed(), function_name);
    }

    // Show config info
    if let Some(ref config_class) = plugin.config_class {
        print!("  {}: {}", "Config Class".dimmed(), config_class);
        if let Some(ref config_module) = plugin.config_module {
            print!(" ({})", config_module);
        }
        println!();
    }

    // Show hooks
    if !plugin.hooks.is_empty() {
        println!("  {}:", "Hooks".dimmed());
        for hook in &plugin.hooks {
            println!("    - {}", hook);
        }
    }

    // Show arguments
    if !plugin.parameters.is_empty() {
        println!("  {}:", "Arguments".dimmed());
        for param in &plugin.parameters {
            let req_marker = if param.required { "*" } else { " " };
            let module_str = param
                .module
                .as_ref()
                .map(|m| format!(" ({})", m))
                .unwrap_or_default();
            let default_str = param
                .default
                .as_ref()
                .map(|d| format!(" = {}", d))
                .unwrap_or_default();
            println!(
                "    {}{}: {}{}{}",
                req_marker,
                param.name,
                param.format_types(),
                module_str,
                default_str
            );

            if let Some(ref desc) = param.description {
                println!("      {}", desc.dimmed());
            }
        }
    }

    // Show config schema if available
    if !plugin.config_schema.is_empty() {
        println!("  {}:", "Config Schema".dimmed());
        for (field_name, field) in plugin.config_schema.iter() {
            let req_marker = if field.required { "*" } else { "" };
            println!("    {}{}: {:?}", field_name, req_marker, field.field_type);
        }
    }
}