Repository: FuelLabs/fuelup (rust)

Issue:
implement `fuelup show`
It would be useful to support `fuelup show` similar to `rustup show` very soon once we support multiple toolchains:

### `rustup show` output

```console
$ rustup show
Default host: x86_64-apple-darwin
rustup home:  $HOME/.rustup

installed toolchains
--------------------

stable-x86_64-apple-darwin (default)
nightly-2022-06-13-x86_64-apple-darwin
nightly-x86_64-apple-darwin

active toolchain
----------------

stable-x86_64-apple-darwin (default)
rustc 1.62.1 (e092d0b6b 2022-07-16)
```

### proposed `fuelup show` output
```console
$ fuelup show
Default host: x86_64-apple-darwin
fuelup home:  $HOME/.fuelup

installed toolchains
--------------------

latest-x86_64-apple-darwin (default)
nightly-2022-06-13-x86_64-apple-darwin
local-dev-custom
another-custom

active toolchain
----------------

latest-x86_64-apple-darwin (default)
  forc 0.18.1 
  fuel-core 0.9.6
```

Pull request:
`fuelup show`
Closes #120

Example output (new, initialised toolchain with nothing installed):

![Screenshot 2022-08-22 at 5 32 32 PM](https://user-images.githubusercontent.com/25565268/185889345-dbbf2d23-ca5e-4a70-809a-486af0cab97a.png)


Example output on `latest` toolchain:

![Screenshot 2022-08-22 at 5 32 58 PM](https://user-images.githubusercontent.com/25565268/185889431-657611ef-55ff-45ee-9066-d2e7bc34085e.png)
 

Patch:
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 9c474749..635de9d9 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -2,4 +2,5 @@ pub mod check;
 pub mod component;
 pub mod default;
 pub mod fuelup;
+pub mod show;
 pub mod toolchain;
diff --git a/src/commands/show.rs b/src/commands/show.rs
new file mode 100644
index 00000000..38e6900b
--- /dev/null
+++ b/src/commands/show.rs
@@ -0,0 +1,11 @@
+use anyhow::Result;
+use clap::Parser;
+
+use crate::ops::fuelup_show;
+
+#[derive(Debug, Parser)]
+pub struct ShowCommand {}
+
+pub fn exec() -> Result<()> {
+    fuelup_show::show()
+}
diff --git a/src/config.rs b/src/config.rs
index 091ad009..9b5a8183 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -18,6 +18,19 @@ impl Config {
         })
     }
 
+    pub(crate) fn list_toolchains(&self) -> Result<Vec<String>> {
+        if self.toolchains_dir.is_dir() {
+            let toolchains: Vec<String> = fs::read_dir(&self.toolchains_dir)?
+                .filter_map(io::Result::ok)
+                .filter(|e| e.file_type().map(|f| f.is_dir()).unwrap_or(false))
+                .map(|e| e.file_name().into_string().ok().unwrap_or_default())
+                .collect();
+            Ok(toolchains)
+        } else {
+            Ok(Vec::new())
+        }
+    }
+
     pub(crate) fn list_official_toolchains(&self) -> Result<Vec<String>> {
         if self.toolchains_dir.is_dir() {
             let toolchains: Vec<String> = fs::read_dir(&self.toolchains_dir)?
diff --git a/src/fmt.rs b/src/fmt.rs
index c7c8a39c..10c6eae2 100644
--- a/src/fmt.rs
+++ b/src/fmt.rs
@@ -1,3 +1,4 @@
+use std::io::Write;
 use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
 
 // In the below functions, we ignore the `Result`s of `set_color` and `reset` to allow `write`
@@ -23,3 +24,10 @@ where
     write(&mut stdout).expect("Unexpected error writing to stdout");
     let _ = stdout.reset();
 }
+
+pub fn print_header(header: &str) {
+    let mut stdout = StandardStream::stdout(ColorChoice::Auto);
+    bold(|s| writeln!(s, "{}", header));
+    writeln!(stdout, "{}", "-".repeat(header.len())).expect("Unexpected error writing to stdout");
+    let _ = stdout.reset();
+}
diff --git a/src/fuelup_cli.rs b/src/fuelup_cli.rs
index 20c98fdb..449686c5 100644
--- a/src/fuelup_cli.rs
+++ b/src/fuelup_cli.rs
@@ -1,7 +1,8 @@
 use anyhow::Result;
 use clap::Parser;
 
-use crate::commands::{check, component, default, fuelup, toolchain};
+use crate::commands::show::ShowCommand;
+use crate::commands::{check, component, default, fuelup, show, toolchain};
 
 use crate::commands::check::CheckCommand;
 use crate::commands::component::ComponentCommand;
@@ -31,6 +32,8 @@ enum Commands {
     /// Install new toolchains or modify/query installed toolchains
     #[clap(subcommand)]
     Toolchain(ToolchainCommand),
+    /// Show the active and installed toolchains, as well as the host and fuelup home
+    Show(ShowCommand),
 }
 
 pub fn fuelup_cli() -> Result<()> {
@@ -43,6 +46,7 @@ pub fn fuelup_cli() -> Result<()> {
         Commands::Fuelup(command) => match command {
             FuelupCommand::Update => fuelup::exec(),
         },
+        Commands::Show(_command) => show::exec(),
         Commands::Toolchain(command) => toolchain::exec(command),
     }
 }
diff --git a/src/ops/fuelup_show.rs b/src/ops/fuelup_show.rs
new file mode 100644
index 00000000..92f87eb3
--- /dev/null
+++ b/src/ops/fuelup_show.rs
@@ -0,0 +1,114 @@
+use anyhow::Result;
+use semver::Version;
+use std::io::Write;
+
+use crate::{
+    component::{self, SUPPORTED_PLUGINS},
+    config::Config,
+    fmt::{bold, print_header},
+    path::fuelup_dir,
+    target_triple::TargetTriple,
+    toolchain::Toolchain,
+};
+
+pub fn show() -> Result<()> {
+    bold(|s| write!(s, "Default host: "));
+    println!("{}", TargetTriple::from_host()?);
+
+    bold(|s| write!(s, "fuelup home: "));
+    println!("{}", fuelup_dir().display());
+    println!();
+
+    print_header("installed toolchains");
+    let cfg = Config::from_env()?;
+    let current_toolchain = Toolchain::from_settings()?;
+
+    for toolchain in cfg.list_toolchains()? {
+        if toolchain == current_toolchain.name {
+            println!("{} (default)", toolchain);
+        } else {
+            println!("{}", toolchain);
+        }
+    }
+
+    println!();
+    print_header("active toolchain");
+    let current_toolchain = Toolchain::from_settings()?;
+
+    println!("{} (default)", current_toolchain.name);
+    for component in [component::FORC, component::FUEL_CORE] {
+        let component_executable = current_toolchain.path.join(component);
+
+        match std::process::Command::new(&component_executable)
+            .arg("--version")
+            .output()
+        {
+            Ok(o) => {
+                let output = String::from_utf8_lossy(&o.stdout).into_owned();
+                match output.split_whitespace().nth(1) {
+                    Some(v) => {
+                        let version = Version::parse(v)?;
+                        bold(|s| write!(s, "  {}", &component));
+                        println!(" : {}", version);
+                    }
+                    None => {
+                        eprintln!("  {} - Error getting version string", component);
+                    }
+                };
+            }
+            Err(e) => {
+                print!("  ");
+                bold(|s| write!(s, "{}", &component));
+                print!(" - ");
+                if component_executable.exists() {
+                    println!("execution error - {}", e);
+                } else {
+                    println!("not found");
+                }
+            }
+        };
+
+        if component == component::FORC {
+            for plugin in SUPPORTED_PLUGINS {
+                let plugin_executable = current_toolchain.path.join(&plugin);
+                if plugin == &component::FORC_DEPLOY {
+                    bold(|s| writeln!(s, "    - forc-client"));
+                }
+                if plugin == &component::FORC_RUN || plugin == &component::FORC_DEPLOY {
+                    print!("  ");
+                }
+                match std::process::Command::new(&plugin_executable)
+                    .arg("--version")
+                    .output()
+                {
+                    Ok(o) => {
+                        let output = String::from_utf8_lossy(&o.stdout).into_owned();
+                        match output.split_whitespace().nth(1) {
+                            Some(v) => {
+                                let version = Version::parse(v)?;
+                                print!("    - ");
+                                bold(|s| write!(s, "{}", plugin));
+                                println!(" : {}", version);
+                            }
+                            None => {
+                                eprintln!("    - {} - Error getting version string", plugin);
+                            }
+                        };
+                    }
+                    Err(e) => {
+                        print!("    - ");
+                        bold(|s| write!(s, "{}", plugin));
+                        print!(" - ");
+                        if plugin_executable.exists() {
+                            println!("execution error - {}", e);
+                        } else {
+                            println!("not found");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 025e4b7f..923f2ed0 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -1,4 +1,5 @@
 pub mod fuelup_check;
 pub mod fuelup_component;
 pub mod fuelup_self;
+pub mod fuelup_show;
 pub mod fuelup_toolchain;


Test patch:
diff --git a/tests/commands.rs b/tests/commands.rs
index 846f7541..3d637a96 100644
--- a/tests/commands.rs
+++ b/tests/commands.rs
@@ -80,6 +80,40 @@ fn fuelup_check() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn fuelup_show() -> Result<()> {
+    testcfg::setup(FuelupState::Empty, &|cfg| {
+        cfg.fuelup(&["toolchain", "new", "my_toolchain"]);
+        let stdout = cfg.fuelup(&["show"]).stdout;
+
+        let mut lines = stdout.lines();
+        assert_eq!(
+            lines.next().unwrap(),
+            &format!("Default host: {}", TargetTriple::from_host().unwrap())
+        );
+        assert!(lines.next().unwrap().contains("fuelup home: "));
+
+        let expected_stdout = r#"installed toolchains
+--------------------
+my_toolchain (default)
+
+active toolchain
+----------------
+my_toolchain (default)
+  forc - not found
+    - forc-fmt - not found
+    - forc-lsp - not found
+    - forc-explore - not found
+    - forc-client
+      - forc-run - not found
+      - forc-deploy - not found
+  fuel-core - not found
+"#;
+        assert!(stdout.contains(expected_stdout));
+    })?;
+    Ok(())
+}
+
 #[test]
 fn fuelup_self_update() -> Result<()> {
     testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| {