use comfy_table::{Attribute, Cell, Color};
use eyre::{ensure, Result};
use indexmap::IndexMap;
use itertools::Itertools;
use rayon::prelude::*;
use serde_derive::Serialize;
use std::path::PathBuf;
use std::sync::Arc;
use versions::Versioning;
use crate::backend::Backend;
use crate::cli::args::BackendArg;
use crate::config;
use crate::config::Config;
use crate::toolset::{ToolSource, ToolVersion, Toolset};
use crate::ui::table::MiseTable;
#[derive(Debug, clap::Args)]
#[clap(visible_alias = "list", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Ls {
#[clap(conflicts_with = "plugin_flag")]
plugin: Option<Vec<BackendArg>>,
#[clap(long = "plugin", short, hide = true)]
plugin_flag: Option<BackendArg>,
#[clap(long, short)]
current: bool,
#[clap(long, short)]
global: bool,
#[clap(long, short)]
installed: bool,
#[clap(long, short)]
offline: bool,
#[clap(long)]
outdated: bool,
#[clap(long, short = 'J')]
json: bool,
#[clap(long, short, conflicts_with = "installed")]
missing: bool,
#[clap(long, requires = "plugin")]
prefix: Option<String>,
#[clap(long, alias = "no-headers", verbatim_doc_comment, conflicts_with_all = &["json"])]
no_header: bool,
}
impl Ls {
pub fn run(mut self) -> Result<()> {
let config = Config::try_get()?;
self.plugin = self
.plugin
.or_else(|| self.plugin_flag.clone().map(|p| vec![p]));
self.verify_plugin()?;
let mut runtimes = self.get_runtime_list(&config)?;
if self.current || self.global {
runtimes.retain(|(_, _, _, source)| !source.is_unknown());
}
if self.installed {
runtimes.retain(|(_, p, tv, _)| p.is_version_installed(tv, true));
}
if self.missing {
runtimes.retain(|(_, p, tv, _)| !p.is_version_installed(tv, true));
}
if let Some(prefix) = &self.prefix {
runtimes.retain(|(_, _, tv, _)| tv.version.starts_with(prefix));
}
if self.json {
self.display_json(runtimes)
} else {
self.display_user(runtimes)
}
}
fn verify_plugin(&self) -> Result<()> {
if let Some(plugins) = &self.plugin {
for ba in plugins {
if let Some(plugin) = ba.backend()?.plugin() {
ensure!(plugin.is_installed(), "{ba} is not installed");
}
}
}
Ok(())
}
fn display_json(&self, runtimes: Vec<RuntimeRow>) -> Result<()> {
if let Some(plugins) = &self.plugin {
let runtimes: Vec<JSONToolVersion> = runtimes
.into_iter()
.filter(|(_, p, _, _)| plugins.contains(p.ba()))
.map(|row| row.into())
.collect();
miseprintln!("{}", serde_json::to_string_pretty(&runtimes)?);
return Ok(());
}
let mut plugins = JSONOutput::new();
for (plugin_name, runtimes) in &runtimes
.into_iter()
.chunk_by(|(_, p, _, _)| p.id().to_string())
{
let runtimes = runtimes.map(|row| row.into()).collect();
plugins.insert(plugin_name.clone(), runtimes);
}
miseprintln!("{}", serde_json::to_string_pretty(&plugins)?);
Ok(())
}
fn display_user(&self, runtimes: Vec<RuntimeRow>) -> Result<()> {
let rows = runtimes
.into_par_iter()
.map(|(ls, p, tv, source)| Row {
tool: p.clone(),
version: (ls, p.as_ref(), &tv, &source).into(),
requested: match source.is_unknown() {
true => None,
false => Some(tv.request.version()),
},
source: if source.is_unknown() {
None
} else {
Some(source)
},
})
.collect::<Vec<_>>();
let mut table = MiseTable::new(self.no_header, &["Tool", "Version", "Source", "Requested"]);
for r in rows {
let row = vec![
r.display_tool(),
r.display_version(),
r.display_source(),
r.display_requested(),
];
table.add_row(row);
}
table.truncate(true).print()
}
fn get_runtime_list(&self, config: &Config) -> Result<Vec<RuntimeRow>> {
let mut trs = config.get_tool_request_set()?.clone();
if self.global {
trs = trs
.iter()
.filter(|(.., ts)| match ts {
ToolSource::MiseToml(p) => config::is_global_config(p),
_ => false,
})
.map(|(fa, tv, ts)| (fa.clone(), tv.clone(), ts.clone()))
.collect()
}
let mut ts = Toolset::from(trs);
ts.resolve()?;
let rvs: Vec<RuntimeRow> = ts
.list_all_versions()?
.into_iter()
.map(|(b, tv)| ((b, tv.version.clone()), tv))
.filter(|((b, _), _)| match &self.plugin {
Some(p) => p.contains(b.ba()),
None => true,
})
.sorted_by_cached_key(|((plugin_name, version), _)| {
(
plugin_name.clone(),
Versioning::new(version),
version.clone(),
)
})
.map(|(k, tv)| (self, k.0, tv.clone(), tv.request.source().clone()))
.filter(|(_ls, p, tv, source)| !source.is_unknown() || p.is_version_installed(tv, true))
.filter(|(_ls, p, _, _)| match &self.plugin {
Some(backend) => backend.contains(p.ba()),
None => true,
})
.collect();
Ok(rvs)
}
}
type JSONOutput = IndexMap<String, Vec<JSONToolVersion>>;
#[derive(Serialize)]
struct JSONToolVersion {
version: String,
#[serde(skip_serializing_if = "Option::is_none")]
requested_version: Option<String>,
install_path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<IndexMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
symlinked_to: Option<PathBuf>,
installed: bool,
active: bool,
}
type RuntimeRow<'a> = (&'a Ls, Arc<dyn Backend>, ToolVersion, ToolSource);
struct Row {
tool: Arc<dyn Backend>,
version: VersionStatus,
source: Option<ToolSource>,
requested: Option<String>,
}
impl Row {
fn display_tool(&self) -> Cell {
Cell::new(&self.tool).fg(Color::Blue)
}
fn display_version(&self) -> Cell {
match &self.version {
VersionStatus::Active(version, outdated) => {
if *outdated {
Cell::new(format!("{version} (outdated)"))
.fg(Color::Yellow)
.add_attribute(Attribute::Bold)
} else {
Cell::new(version).fg(Color::Green)
}
}
VersionStatus::Inactive(version) => Cell::new(version).add_attribute(Attribute::Dim),
VersionStatus::Missing(version) => Cell::new(format!("{version} (missing)"))
.fg(Color::Red)
.add_attribute(Attribute::CrossedOut),
VersionStatus::Symlink(version, active) => {
let mut cell = Cell::new(format!("{version} (symlink)"));
if !*active {
cell = cell.add_attribute(Attribute::Dim);
}
cell
}
}
}
fn display_source(&self) -> Cell {
Cell::new(match &self.source {
Some(source) => source.to_string(),
None => String::new(),
})
}
fn display_requested(&self) -> Cell {
Cell::new(match &self.requested {
Some(s) => s.clone(),
None => String::new(),
})
}
}
impl From<RuntimeRow<'_>> for JSONToolVersion {
fn from(row: RuntimeRow) -> Self {
let (ls, p, tv, source) = row;
let vs: VersionStatus = (ls, p.as_ref(), &tv, &source).into();
JSONToolVersion {
symlinked_to: p.symlink_path(&tv),
install_path: tv.install_path(),
version: tv.version.clone(),
requested_version: if source.is_unknown() {
None
} else {
Some(tv.request.version())
},
source: if source.is_unknown() {
None
} else {
Some(source.as_json())
},
installed: !matches!(vs, VersionStatus::Missing(_)),
active: matches!(vs, VersionStatus::Active(_, _)),
}
}
}
enum VersionStatus {
Active(String, bool),
Inactive(String),
Missing(String),
Symlink(String, bool),
}
impl From<(&Ls, &dyn Backend, &ToolVersion, &ToolSource)> for VersionStatus {
fn from((ls, p, tv, source): (&Ls, &dyn Backend, &ToolVersion, &ToolSource)) -> Self {
if p.symlink_path(tv).is_some() {
VersionStatus::Symlink(tv.version.clone(), !source.is_unknown())
} else if !p.is_version_installed(tv, true) {
VersionStatus::Missing(tv.version.clone())
} else if !source.is_unknown() {
let outdated = if ls.outdated {
p.is_version_outdated(tv)
} else {
false
};
VersionStatus::Active(tv.version.clone(), outdated)
} else {
VersionStatus::Inactive(tv.version.clone())
}
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>mise ls</bold>
node 20.0.0 ~/src/myapp/.tool-versions latest
python 3.11.0 ~/.tool-versions 3.10
python 3.10.0
$ <bold>mise ls --current</bold>
node 20.0.0 ~/src/myapp/.tool-versions 20
python 3.11.0 ~/.tool-versions 3.11.0
$ <bold>mise ls --json</bold>
{
"node": [
{
"version": "20.0.0",
"install_path": "/Users/jdx/.mise/installs/node/20.0.0",
"source": {
"type": "mise.toml",
"path": "/Users/jdx/mise.toml"
}
}
],
"python": [...]
}
"#
);