midenup 1.1.0

The Miden toolchain manager
use clap::Subcommand;
use colored::Colorize;

use super::Flags;
use crate::{
    channel::UpstreamMatch,
    config::Config,
    report,
    state::LocalState,
    toolchain::{Toolchain, ToolchainJustification},
};

/// Appends a status marker to a listing row: yellow as one span when color is on, plain otherwise.
fn push_marker(line: &mut String, text: &str, use_color: bool) {
    use core::fmt::Write;

    if use_color {
        write!(line, " {}", text.yellow()).unwrap();
    } else {
        write!(line, " {text}").unwrap();
    }
}

#[derive(Debug, Subcommand)]
pub enum ShowCommand {
    /// Show the active toolchain.
    #[command(name = "active-toolchain")]
    Current {
        #[clap(flatten)]
        flags: Flags,
    },
    /// Display the computed value of MIDENUP_HOME
    Home,
    /// List installed toolchains
    List {
        #[clap(flatten)]
        flags: Flags,
    },
}

impl ShowCommand {
    pub fn execute(&self, config: &Config, state: &LocalState) -> anyhow::Result<()> {
        use core::fmt::Write;

        match self {
            Self::Current { .. } => {
                let (toolchain, justification) = Toolchain::current(config, None)?;

                // The justification is commentary, not the result, so it goes to stderr.
                if report::verbosity() >= report::Verbosity::Debug {
                    match justification {
                        ToolchainJustification::MidenToolchainFile { path } => {
                            crate::info!("found a miden-toolchain.toml file in {}", path.display())
                        },
                        ToolchainJustification::Override => {
                            crate::info!(
                                "system default has been overridden via `midenup override`"
                            )
                        },
                        ToolchainJustification::Requested => {
                            crate::info!("explicitly requested by user")
                        },
                        ToolchainJustification::Default => {
                            crate::info!("current toolchain is system default")
                        },
                    }
                }
                println!("{}", toolchain.channel);

                Ok(())
            },
            Self::Home => {
                println!("{}", config.midenup_home.display());

                Ok(())
            },
            Self::List { .. } => {
                // Installed toolchains are recorded locally, so this works with no network at all.
                // Upstream only adds *markers* -- which networks name a channel, which
                // installations have updates or are no longer published -- so when it is
                // unavailable they are simply omitted rather than guessed at.
                let upstream = config.upstream_manifest().ok();
                // The upstream lookup may have emitted a report to stderr, so restore stdout's
                // color policy immediately before rendering the result.
                let use_color = report::prepare_stdout_color();

                // Every `toolchains/<network>` link on this machine: the networks the user
                // installed, and what to compare with upstream.
                let local_links = crate::networks::links(&config.midenup_home);

                let toolchains_display: Vec<_> = state
                    .installations
                    .iter()
                    .map(|installation| {
                        let name = &installation.channel;
                        let mut line = format!("{name}");

                        // The networks the user installed onto this channel, not every network
                        // upstream says runs it. Several may share one channel, so this is a
                        // list rather than a single marker.
                        let networks: Vec<&str> = local_links
                            .iter()
                            .filter(|(_, linked)| *linked == name)
                            .map(|(network, _)| network.as_str())
                            .collect();
                        if !networks.is_empty() {
                            if use_color {
                                write!(
                                    &mut line,
                                    " {}",
                                    format!("({})", networks.join(", ")).bold()
                                )
                                .unwrap();
                            } else {
                                write!(&mut line, " ({})", networks.join(", ")).unwrap();
                            }
                        }

                        // If a network whose link still names this channel while upstream has
                        // moved it, we show it to the user.
                        if let Some(manifest) = upstream {
                            for (network, linked) in &local_links {
                                if linked != name {
                                    continue;
                                }
                                if let Some(notice) = crate::networks::drift(
                                    network,
                                    linked,
                                    manifest.network_version(network),
                                ) {
                                    push_marker(&mut line, &notice, use_color);
                                }
                            }
                        }

                        // A migrated record describes a pre-publication tree that no receipt
                        // covers, so midenup will not execute against it. Saying so is the whole
                        // point: the user's toolchain still works, but only after it is installed
                        // properly, and they should not have to infer that from a failure.
                        if !installation.is_managed() {
                            push_marker(
                                &mut line,
                                &format!("(needs reinstallation) -- run `midenup install {name}`"),
                                use_color,
                            );
                        }

                        if upstream.is_some() {
                            // The lookup follows migration lineage the way `update` does, so a
                            // superseded channel shows as updatable rather than gone. Verbatim
                            // manifest content: a listing pins nothing and reaches for no source.
                            match installation.as_channel().upstream_counterpart_raw(config) {
                                // An unmanaged record already carries its one instruction, the
                                // reinstall above; a second directive would contradict it.
                                Some(_) if !installation.is_managed() => {},
                                Some((channel, upstream_match)) => {
                                    let has_update = match upstream_match {
                                        // Being superseded is the update: running it migrates.
                                        UpstreamMatch::Migrated { .. } => true,
                                        UpstreamMatch::UpstreamCounterpart => {
                                            super::update::needs_update(
                                                config,
                                                installation,
                                                channel,
                                            )
                                        },
                                    };
                                    if has_update {
                                        push_marker(
                                            &mut line,
                                            &format!(
                                                "(update available -- run `midenup update {name}`)"
                                            ),
                                            use_color,
                                        );
                                    }
                                },
                                // Retained, not deleted: the user may still want `var/` and an
                                // explicit uninstall (spec section 12.3).
                                None => push_marker(&mut line, "(unavailable upstream)", use_color),
                            }
                        }

                        line
                    })
                    .collect();

                if use_color {
                    println!("{}", "Installed toolchains:".bold().underline());
                } else {
                    println!("Installed toolchains:");
                }
                for toolchain in toolchains_display {
                    println!("{toolchain}");
                }

                Ok(())
            },
        }
    }
}