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;
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 let Some(ref plugin_name) = plugin_filter {
return show_plugin_details(
manifest,
plugin_name,
module_filter.as_deref(),
opts.verbose,
ctx,
);
}
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());
let python_path = &ctx.python_path;
let uv_path = &ctx.uv_path;
for (package_name, plugin_names) in &packages {
let pkg = manifest
.packages
.iter()
.find(|p| p.name.as_ref() == package_name);
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> {
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))
})?;
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!();
let plugins_to_show: Vec<_> = if let Some(module_name) = module_filter {
package
.plugins
.iter()
.filter(|p| {
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
);
println!(" {}: {}", "Module".dimmed(), plugin.module);
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);
}
if let Some(ref config_class) = plugin.config_class {
println!(" {}: {}", "Config".dimmed(), config_class);
}
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);
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);
}
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!();
}
if !plugin.hooks.is_empty() {
println!(" {}:", "Hooks".dimmed());
for hook in &plugin.hooks {
println!(" - {}", hook);
}
}
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());
}
}
}
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);
}
}
}