ソースを参照

refactor(cli): cleanup info command (#7204)

Co-authored-by: martin frances <martinfrances107@hotmail.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 1 年間 前
コミット
99865d9e9a

+ 13 - 35
tooling/cli/src/info/app.rs

@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{SectionItem, Status};
+use super::SectionItem;
 use crate::helpers::framework;
 use std::{fs::read_to_string, path::PathBuf};
 
@@ -14,15 +14,11 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<PathBuf>) -> Vec<Secti
       let config = config_guard.as_ref().unwrap();
 
       let bundle_or_build = if config.tauri.bundle.active {
-        "bundle".to_string()
+        "bundle"
       } else {
-        "build".to_string()
+        "build"
       };
-      items.push(SectionItem::new(
-        move || Some((format!("build-type: {bundle_or_build}"), Status::Neutral)),
-        || None,
-        false,
-      ));
+      items.push(SectionItem::new().description(format!("build-type: {bundle_or_build}")));
 
       let csp = config
         .tauri
@@ -31,42 +27,24 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<PathBuf>) -> Vec<Secti
         .clone()
         .map(|c| c.to_string())
         .unwrap_or_else(|| "unset".to_string());
-      items.push(SectionItem::new(
-        move || Some((format!("CSP: {csp}"), Status::Neutral)),
-        || None,
-        false,
-      ));
+      items.push(SectionItem::new().description(format!("CSP: {csp}")));
 
-      let dist_dir = config.build.dist_dir.to_string();
-      items.push(SectionItem::new(
-        move || Some((format!("distDir: {dist_dir}"), Status::Neutral)),
-        || None,
-        false,
-      ));
+      let dist_dir = &config.build.dist_dir;
+      items.push(SectionItem::new().description(format!("distDir: {dist_dir}")));
 
-      let dev_path = config.build.dev_path.to_string();
-      items.push(SectionItem::new(
-        move || Some((format!("devPath: {dev_path}"), Status::Neutral)),
-        || None,
-        false,
-      ));
+      let dev_path = &config.build.dev_path;
+      items.push(SectionItem::new().description(format!("devPath: {dev_path}")));
 
       if let Some(app_dir) = app_dir {
         if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
           let (framework, bundler) = framework::infer_from_package_json(&package_json);
+
           if let Some(framework) = framework {
-            items.push(SectionItem::new(
-              move || Some((format!("framework: {framework}"), Status::Neutral)),
-              || None,
-              false,
-            ));
+            items.push(SectionItem::new().description(format!("framework: {framework}")));
           }
+
           if let Some(bundler) = bundler {
-            items.push(SectionItem::new(
-              move || Some((format!("bundler: {bundler}"), Status::Neutral)),
-              || None,
-              false,
-            ));
+            items.push(SectionItem::new().description(format!("bundler: {bundler}")));
           }
         }
       }

+ 53 - 125
tooling/cli/src/info/env_nodejs.rs

@@ -2,12 +2,11 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{cross_command, VersionMetadata};
-use super::{SectionItem, Status};
+use super::{cross_command, ActionResult, SectionItem, VersionMetadata};
 use colored::Colorize;
 
-pub fn items(metadata: &VersionMetadata) -> (Vec<SectionItem>, Option<String>) {
-  let yarn_version = cross_command("yarn")
+pub fn manager_version(package_manager: &str) -> Option<String> {
+  cross_command(package_manager)
     .arg("-v")
     .output()
     .map(|o| {
@@ -19,129 +18,58 @@ pub fn items(metadata: &VersionMetadata) -> (Vec<SectionItem>, Option<String>) {
       }
     })
     .ok()
-    .unwrap_or_default();
-  let yarn_version_c = yarn_version.clone();
+    .unwrap_or_default()
+}
+
+pub fn items(metadata: &VersionMetadata) -> Vec<SectionItem> {
   let node_target_ver = metadata.js_cli.node.replace(">= ", "");
 
-  (
-    vec![
-      SectionItem::new(
-        move || {
-          cross_command("node")
-            .arg("-v")
-            .output()
-            .map(|o| {
-              if o.status.success() {
-                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
-                let v = v
-                  .split('\n')
-                  .next()
-                  .unwrap()
-                  .strip_prefix('v')
-                  .unwrap_or_default()
-                  .trim();
-                Some((
-                  format!("node: {}{}", v, {
-                    let version = semver::Version::parse(v).unwrap();
-                    let target_version = semver::Version::parse(node_target_ver.as_str()).unwrap();
-                    if version < target_version {
-                      format!(
-                        " ({}, latest: {})",
-                        "outdated".red(),
-                        target_version.to_string().green()
-                      )
-                    } else {
-                      "".into()
-                    }
-                  }),
-                  Status::Neutral,
-                ))
-              } else {
-                None
-              }
-            })
-            .ok()
-            .unwrap_or_default()
-        },
-        || None,
-        false,
-      ),
-      SectionItem::new(
-        || {
-          cross_command("pnpm")
-            .arg("-v")
-            .output()
-            .map(|o| {
-              if o.status.success() {
-                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
-                Some((
-                  format!("pnpm: {}", v.split('\n').next().unwrap()),
-                  Status::Neutral,
-                ))
-              } else {
-                None
-              }
-            })
-            .ok()
-            .unwrap_or_default()
-        },
-        || None,
-        false,
-      ),
-      SectionItem::new(
-        || {
-          cross_command("bun")
-            .arg("-v")
-            .output()
-            .map(|o| {
-              if o.status.success() {
-                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
-                Some((
-                  format!("bun: {}", v.split('\n').next().unwrap()),
-                  Status::Neutral,
-                ))
-              } else {
-                None
-              }
-            })
-            .ok()
-            .unwrap_or_default()
-        },
-        || None,
-        false,
-      ),
-      SectionItem::new(
-        move || {
-          yarn_version_c
-            .as_ref()
-            .map(|v| (format!("yarn: {v}"), Status::Neutral))
-        },
-        || None,
-        false,
-      ),
-      SectionItem::new(
-        || {
-          cross_command("npm")
-            .arg("-v")
-            .output()
-            .map(|o| {
-              if o.status.success() {
-                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
-                Some((
-                  format!("npm: {}", v.split('\n').next().unwrap()),
-                  Status::Neutral,
-                ))
+  vec![
+    SectionItem::new().action(move || {
+      cross_command("node")
+        .arg("-v")
+        .output()
+        .map(|o| {
+          if o.status.success() {
+            let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
+            let v = v
+              .split('\n')
+              .next()
+              .unwrap()
+              .strip_prefix('v')
+              .unwrap_or_default()
+              .trim();
+            ActionResult::Description(format!("node: {}{}", v, {
+              let version = semver::Version::parse(v).unwrap();
+              let target_version = semver::Version::parse(node_target_ver.as_str()).unwrap();
+              if version < target_version {
+                format!(
+                  " ({}, latest: {})",
+                  "outdated".red(),
+                  target_version.to_string().green()
+                )
               } else {
-                None
+                "".into()
               }
-            })
-            .ok()
-            .unwrap_or_default()
-        },
-        || None,
-        false,
-      ),
-    ],
-    yarn_version,
-  )
+            }))
+          } else {
+            ActionResult::None
+          }
+        })
+        .ok()
+        .unwrap_or_default()
+    }),
+    SectionItem::new().action(|| {
+      manager_version("pnpm")
+        .map(|v| format!("pnpm: {}", v))
+        .into()
+    }),
+    SectionItem::new().action(|| {
+      manager_version("yarn")
+        .map(|v| format!("yarn: {}", v))
+        .into()
+    }),
+    SectionItem::new().action(|| manager_version("npm").map(|v| format!("npm: {}", v)).into()),
+    SectionItem::new().action(|| manager_version("bun").map(|v| format!("bun: {}", v)).into()),
+  ]
 }

+ 50 - 100
tooling/cli/src/info/env_rust.rs

@@ -7,95 +7,55 @@ use super::Status;
 use colored::Colorize;
 use std::process::Command;
 
+fn component_version(component: &str) -> Option<(String, Status)> {
+  Command::new(component)
+    .arg("-V")
+    .output()
+    .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
+    .map(|v| {
+      format!(
+        "{component}: {}",
+        v.split('\n')
+          .next()
+          .unwrap()
+          .strip_prefix(&format!("{component} "))
+          .unwrap_or_default()
+      )
+    })
+    .map(|desc| (desc, Status::Success))
+    .ok()
+}
+
 pub fn items() -> Vec<SectionItem> {
   vec![
-    SectionItem::new(
-      || {
-        Some(
-          Command::new("rustc")
-            .arg("-V")
-            .output()
-            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
-            .map(|v| {
-              format!(
-                "rustc: {}",
-                v.split('\n')
-                  .next()
-                  .unwrap()
-                  .strip_prefix("rustc ")
-                  .unwrap_or_default()
-              )
-            })
-            .map(|desc| (desc, Status::Success))
-            .ok()
-            .unwrap_or_else(|| {
-              (
-                format!(
-                  "rustc: {}\nMaybe you don't have rust installed! Visit {}",
-                  "not installed!".red(),
-                  "https://rustup.rs/".cyan()
-                ),
-                Status::Error,
-              )
-            }),
-        )
-      },
-      || None,
-      false,
-    ),
-    SectionItem::new(
-      || {
-        Some(
-          Command::new("cargo")
-            .arg("-V")
-            .output()
-            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
-            .map(|v| {
+    SectionItem::new().action(|| {
+       component_version("rustc")
+          .unwrap_or_else(|| {
+            (
               format!(
-                "Cargo: {}",
-                v.split('\n')
-                  .next()
-                  .unwrap()
-                  .strip_prefix("cargo ")
-                  .unwrap_or_default()
-              )
-            })
-            .map(|desc| (desc, Status::Success))
-            .ok()
-            .unwrap_or_else(|| {
-              (
-                format!(
-                  "Cargo: {}\nMaybe you don't have rust installed! Visit {}",
-                  "not installed!".red(),
-                  "https://rustup.rs/".cyan()
-                ),
-                Status::Error,
-              )
-            }),
-        )
-      },
-      || None,
-      false,
-    ),
-    SectionItem::new(
-      || {
-        Some(
-          Command::new("rustup")
-            .arg("-V")
-            .output()
-            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
-            .map(|v| {
+                "rustc: {}\nMaybe you don't have rust installed! Visit {}",
+                "not installed!".red(),
+                "https://rustup.rs/".cyan()
+              ),
+              Status::Error,
+            )
+          }).into()
+    }),
+    SectionItem::new().action(|| {
+        component_version("cargo")
+          .unwrap_or_else(|| {
+            (
               format!(
-                "rustup: {}",
-                v.split('\n')
-                  .next()
-                  .unwrap()
-                  .strip_prefix("rustup ")
-                  .unwrap_or_default()
-              )
-            })
-            .map(|desc| (desc, Status::Success))
-            .ok()
+                "Cargo: {}\nMaybe you don't have rust installed! Visit {}",
+                "not installed!".red(),
+                "https://rustup.rs/".cyan()
+              ),
+              Status::Error,
+            )
+          }).into()
+    }),
+    SectionItem::new().action(|| {
+        component_version("rustup")
             .unwrap_or_else(|| {
               (
                 format!(
@@ -105,15 +65,9 @@ pub fn items() -> Vec<SectionItem> {
                 ),
                 Status::Warning,
               )
-            }),
-        )
-      },
-      || None,
-      false,
-    ),
-    SectionItem::new(
-      || {
-        Some(
+            }).into()
+    }),
+    SectionItem::new().action(|| {
           Command::new("rustup")
             .args(["show", "active-toolchain"])
             .output()
@@ -135,11 +89,7 @@ pub fn items() -> Vec<SectionItem> {
                 ),
                 Status::Warning,
               )
-            }),
-        )
-      },
-      || None,
-      false,
-    ),
+            }).into()
+    }),
   ]
 }

+ 50 - 83
tooling/cli/src/info/env_system.rs

@@ -2,8 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::SectionItem;
-use super::Status;
+use super::{SectionItem, Status};
 use colored::Colorize;
 #[cfg(windows)]
 use serde::Deserialize;
@@ -177,74 +176,55 @@ fn is_xcode_command_line_tools_installed() -> bool {
 
 pub fn items() -> Vec<SectionItem> {
   vec![
-    SectionItem::new(
-      || {
-        let os_info = os_info::get();
-        Some((
-          format!(
-            "OS: {} {} {:?}",
-            os_info.os_type(),
-            os_info.version(),
-            os_info.bitness()
-          ),
-          Status::Neutral,
-        ))
-      },
-      || None,
-      false,
-    ),
+    SectionItem::new().action(|| {
+      let os_info = os_info::get();
+      format!(
+        "OS: {} {} {:?}",
+        os_info.os_type(),
+        os_info.version(),
+        os_info.bitness()
+      ).into()
+    }),
     #[cfg(windows)]
-    SectionItem::new(
-      || {
-        let error = || {
-          format!(
-            "Webview2: {}\nVisit {}",
-            "not installed!".red(),
-            "https://developer.microsoft.com/en-us/microsoft-edge/webview2/".cyan()
-          )
-        };
-        Some(
-          webview2_version()
-            .map(|v| {
-              v.map(|v| (format!("WebView2: {}", v), Status::Success))
-                .unwrap_or_else(|| (error(), Status::Error))
-            })
-            .unwrap_or_else(|_| (error(), Status::Error)),
-        )
-      },
-      || None,
-      false,
-    ),
+    SectionItem::new().action(|| {
+      let error = format!(
+          "Webview2: {}\nVisit {}",
+          "not installed!".red(),
+          "https://developer.microsoft.com/en-us/microsoft-edge/webview2/".cyan()
+        );
+      webview2_version()
+        .map(|v| {
+          v.map(|v| (format!("WebView2: {}", v), Status::Success))
+            .unwrap_or_else(|| (error.clone(), Status::Error))
+        })
+        .unwrap_or_else(|_| (error, Status::Error)).into()
+    }),
     #[cfg(windows)]
-    SectionItem::new(
-      || {
-        let build_tools = build_tools_version().unwrap_or_default();
-        if build_tools.is_empty() {
-          Some((
+    SectionItem::new().action(|| {
+      let build_tools = build_tools_version().unwrap_or_default();
+      if build_tools.is_empty() {
+        (
             format!(
               "Couldn't detect any Visual Studio or VS Build Tools instance with MSVC and SDK components. Download from {}",
               "https://aka.ms/vs/17/release/vs_BuildTools.exe".cyan()
             ),
             Status::Error,
-          ))
-        } else {
-          Some((
-            format!(
-              "MSVC: {}{}",
-              if build_tools.len() > 1 {
-                format!("\n  {} ", "-".cyan())
-              } else {
-                "".into()
-              },
-              build_tools.join(format!("\n  {} ", "-".cyan()).as_str()),
-            ),
-            Status::Success,
-          ))
-        }
-      },
-      || None,
-      false,
-    ),
+          ).into()
+      } else {
+        (
+          format!(
+            "MSVC: {}{}",
+            if build_tools.len() > 1 {
+              format!("\n  {} ", "-".cyan())
+            } else {
+              "".into()
+            },
+            build_tools.join(format!("\n  {} ", "-".cyan()).as_str()),
+          ),
+          Status::Success,
+        ).into()
+      }
+    }),
     #[cfg(any(
       target_os = "linux",
       target_os = "dragonfly",
@@ -252,9 +232,7 @@ pub fn items() -> Vec<SectionItem> {
       target_os = "openbsd",
       target_os = "netbsd"
     ))]
-    SectionItem::new(
-      || {
-        Some(
+    SectionItem::new().action(|| {
           webkit2gtk_ver()
             .map(|v| (format!("webkit2gtk-4.0: {v}"), Status::Success))
             .unwrap_or_else(|| {
@@ -266,11 +244,8 @@ pub fn items() -> Vec<SectionItem> {
                 ),
                 Status::Error,
               )
-            }),
-        )
+            }).into()
       },
-      || None,
-      false,
     ),
     #[cfg(any(
       target_os = "linux",
@@ -279,9 +254,7 @@ pub fn items() -> Vec<SectionItem> {
       target_os = "openbsd",
       target_os = "netbsd"
     ))]
-    SectionItem::new(
-      || {
-        Some(
+    SectionItem::new().action(|| {
           rsvg2_ver()
             .map(|v| (format!("rsvg2: {v}"), Status::Success))
             .unwrap_or_else(|| {
@@ -293,16 +266,12 @@ pub fn items() -> Vec<SectionItem> {
                 ),
                 Status::Error,
               )
-            }),
-        )
+            }).into()
       },
-      || None,
-      false,
     ),
     #[cfg(target_os = "macos")]
-    SectionItem::new(
-      || {
-        Some(if is_xcode_command_line_tools_installed() {
+    SectionItem::new().action(|| {
+        if is_xcode_command_line_tools_installed() {
           (
             "Xcode Command Line Tools: installed".into(),
             Status::Success,
@@ -316,10 +285,8 @@ pub fn items() -> Vec<SectionItem> {
             ),
             Status::Error,
           )
-        })
+        }.into()
       },
-      || None,
-      false,
     ),
   ]
 }

+ 116 - 34
tooling/cli/src/info/mod.rs

@@ -4,7 +4,7 @@
 
 use crate::Result;
 use clap::Parser;
-use colored::Colorize;
+use colored::{ColoredString, Colorize};
 use dialoguer::{theme::ColorfulTheme, Confirm};
 use serde::Deserialize;
 use std::{
@@ -92,6 +92,18 @@ pub enum Status {
   Error,
 }
 
+impl Status {
+  fn color<S: AsRef<str>>(&self, s: S) -> ColoredString {
+    let s = s.as_ref();
+    match self {
+      Status::Neutral => s.normal(),
+      Status::Success => s.green(),
+      Status::Warning => s.yellow(),
+      Status::Error => s.red(),
+    }
+  }
+}
+
 impl Display for Status {
   fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     write!(
@@ -107,15 +119,55 @@ impl Display for Status {
   }
 }
 
+#[derive(Default)]
+pub enum ActionResult {
+  Full {
+    description: String,
+    status: Status,
+  },
+  Description(String),
+  #[default]
+  None,
+}
+
+impl From<String> for ActionResult {
+  fn from(value: String) -> Self {
+    ActionResult::Description(value)
+  }
+}
+
+impl From<(String, Status)> for ActionResult {
+  fn from(value: (String, Status)) -> Self {
+    ActionResult::Full {
+      description: value.0,
+      status: value.1,
+    }
+  }
+}
+
+impl From<Option<String>> for ActionResult {
+  fn from(value: Option<String>) -> Self {
+    value.map(ActionResult::Description).unwrap_or_default()
+  }
+}
+
+impl From<Option<(String, Status)>> for ActionResult {
+  fn from(value: Option<(String, Status)>) -> Self {
+    value
+      .map(|v| ActionResult::Full {
+        description: v.0,
+        status: v.1,
+      })
+      .unwrap_or_default()
+  }
+}
+
 pub struct SectionItem {
   /// If description is none, the item is skipped
   description: Option<String>,
   status: Status,
-  /// This closure return will be assigned to status and description
-  action: Box<dyn FnMut() -> Option<(String, Status)>>,
-  /// This closure return will be assigned to status and description
-  action_if_err: Box<dyn FnMut() -> Option<(String, Status)>>,
-  has_action_if_err: bool,
+  action: Option<Box<dyn FnMut() -> ActionResult>>,
+  action_if_err: Option<Box<dyn FnMut() -> ActionResult>>,
 }
 
 impl Display for SectionItem {
@@ -131,29 +183,66 @@ impl Display for SectionItem {
 }
 
 impl SectionItem {
-  fn new<
-    F1: FnMut() -> Option<(String, Status)> + 'static,
-    F2: FnMut() -> Option<(String, Status)> + 'static,
-  >(
-    action: F1,
-    action_if_err: F2,
-    has_action_if_err: bool,
-  ) -> Self {
+  fn new() -> Self {
     Self {
-      action: Box::new(action),
-      action_if_err: Box::new(action_if_err),
-      has_action_if_err,
+      action: None,
+      action_if_err: None,
       description: None,
       status: Status::Neutral,
     }
   }
-  fn run(&mut self, interactive: bool) -> Status {
-    if let Some(ret) = (self.action)() {
-      self.description = Some(ret.0);
-      self.status = ret.1;
+
+  fn action<F: FnMut() -> ActionResult + 'static>(mut self, action: F) -> Self {
+    self.action = Some(Box::new(action));
+    self
+  }
+
+  // fn action_if_err<F: FnMut() -> ActionResult + 'static>(mut self, action: F) -> Self {
+  //   self.action_if_err = Some(Box::new(action));
+  //   self
+  // }
+
+  fn description<S: AsRef<str>>(mut self, description: S) -> Self {
+    self.description = Some(description.as_ref().to_string());
+    self
+  }
+
+  fn run_action(&mut self) {
+    let mut res = ActionResult::None;
+    if let Some(action) = &mut self.action {
+      res = action();
     }
+    self.apply_action_result(res);
+  }
 
-    if self.status == Status::Error && interactive && self.has_action_if_err {
+  fn run_action_if_err(&mut self) {
+    let mut res = ActionResult::None;
+    if let Some(action) = &mut self.action_if_err {
+      res = action();
+    }
+    self.apply_action_result(res);
+  }
+
+  fn apply_action_result(&mut self, result: ActionResult) {
+    match result {
+      ActionResult::Full {
+        description,
+        status,
+      } => {
+        self.description = Some(description);
+        self.status = status;
+      }
+      ActionResult::Description(description) => {
+        self.description = Some(description);
+      }
+      ActionResult::None => {}
+    }
+  }
+
+  fn run(&mut self, interactive: bool) -> Status {
+    self.run_action();
+
+    if self.status == Status::Error && interactive && self.action_if_err.is_some() {
       if let Some(description) = &self.description {
         let confirmed = Confirm::with_theme(&ColorfulTheme::default())
           .with_prompt(format!(
@@ -163,13 +252,11 @@ impl SectionItem {
           .interact()
           .unwrap_or(false);
         if confirmed {
-          if let Some(ret) = (self.action_if_err)() {
-            self.description = Some(ret.0);
-            self.status = ret.1;
-          }
+          self.run_action_if_err()
         }
       }
     }
+
     self.status
   }
 }
@@ -192,12 +279,7 @@ impl Section<'_> {
     }
 
     let status_str = format!("[{status}]");
-    let status = match status {
-      Status::Neutral => status_str.normal(),
-      Status::Success => status_str.green(),
-      Status::Warning => status_str.yellow(),
-      Status::Error => status_str.red(),
-    };
+    let status = status.color(status_str);
 
     println!();
     println!("{} {}", status, self.label.bold().yellow());
@@ -239,7 +321,7 @@ pub fn command(options: Options) -> Result<()> {
   };
   environment.items.extend(env_system::items());
   environment.items.extend(env_rust::items());
-  let (items, yarn_version) = env_nodejs::items(&metadata);
+  let items = env_nodejs::items(&metadata);
   environment.items.extend(items);
 
   let mut packages = Section {
@@ -252,7 +334,7 @@ pub fn command(options: Options) -> Result<()> {
     .extend(packages_rust::items(app_dir, tauri_dir.clone()));
   packages
     .items
-    .extend(packages_nodejs::items(app_dir, &metadata, yarn_version));
+    .extend(packages_nodejs::items(app_dir, &metadata));
 
   let mut app = Section {
     label: "App",

+ 35 - 45
tooling/cli/src/info/packages_nodejs.rs

@@ -2,8 +2,8 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{cross_command, VersionMetadata};
-use super::{SectionItem, Status};
+use super::SectionItem;
+use super::{cross_command, env_nodejs::manager_version, VersionMetadata};
 use colored::Colorize;
 use serde::Deserialize;
 use std::fmt::Display;
@@ -241,11 +241,7 @@ fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
   }
 }
 
-pub fn items(
-  app_dir: Option<&PathBuf>,
-  metadata: &VersionMetadata,
-  yarn_version: Option<String>,
-) -> Vec<SectionItem> {
+pub fn items(app_dir: Option<&PathBuf>, metadata: &VersionMetadata) -> Vec<SectionItem> {
   let mut package_manager = PackageManager::Npm;
   if let Some(app_dir) = &app_dir {
     let app_dir_entries = std::fs::read_dir(app_dir)
@@ -256,7 +252,7 @@ pub fn items(
   }
 
   if package_manager == PackageManager::Yarn
-    && yarn_version
+    && manager_version("yarn")
       .map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
       .unwrap_or(false)
   {
@@ -270,46 +266,40 @@ pub fn items(
       ("@tauri-apps/cli", Some(metadata.js_cli.version.clone())),
     ] {
       let app_dir = app_dir.clone();
-      let item = SectionItem::new(
-        move || {
-          let version = version.clone().unwrap_or_else(|| {
-            npm_package_version(&package_manager, package, &app_dir)
-              .unwrap_or_default()
-              .unwrap_or_default()
-          });
-          let latest_ver = npm_latest_version(&package_manager, package)
+      let item = SectionItem::new().action(move || {
+        let version = version.clone().unwrap_or_else(|| {
+          npm_package_version(&package_manager, package, &app_dir)
             .unwrap_or_default()
-            .unwrap_or_default();
+            .unwrap_or_default()
+        });
+        let latest_ver = npm_latest_version(&package_manager, package)
+          .unwrap_or_default()
+          .unwrap_or_default();
 
-          Some((
-            if version.is_empty() {
-              format!("{} {}: not installed!", package, "[NPM]".dimmed())
-            } else {
-              format!(
-                "{} {}: {}{}",
-                package,
-                "[NPM]".dimmed(),
-                version,
-                if !(version.is_empty() || latest_ver.is_empty()) {
-                  let version = semver::Version::parse(version.as_str()).unwrap();
-                  let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
+        if version.is_empty() {
+          format!("{} {}: not installed!", package, "".green())
+        } else {
+          format!(
+            "{} {}: {}{}",
+            package,
+            "[NPM]".dimmed(),
+            version,
+            if !(version.is_empty() || latest_ver.is_empty()) {
+              let version = semver::Version::parse(version.as_str()).unwrap();
+              let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
 
-                  if version < target_version {
-                    format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
-                  } else {
-                    "".into()
-                  }
-                } else {
-                  "".into()
-                }
-              )
-            },
-            Status::Neutral,
-          ))
-        },
-        || None,
-        false,
-      );
+              if version < target_version {
+                format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
+              } else {
+                "".into()
+              }
+            } else {
+              "".into()
+            }
+          )
+        }
+        .into()
+      });
 
       items.push(item);
     }

+ 46 - 84
tooling/cli/src/info/packages_rust.rs

@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{SectionItem, Status};
+use super::{ActionResult, SectionItem};
 use crate::interface::rust::get_workspace_dir;
 use colored::Colorize;
 use serde::Deserialize;
@@ -212,96 +212,58 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<PathBuf>) -> Vec<Secti
       for dep in ["tauri", "tauri-build", "wry", "tao"] {
         let (version_string, version_suffix) =
           crate_version(&tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
-        let dep = dep.to_string();
-        let item = SectionItem::new(
-          move || {
-            Some((
-              format!(
-                "{} {}: {}{}",
-                dep,
-                "[RUST]".dimmed(),
-                version_string,
-                version_suffix
-                  .clone()
-                  .map(|s| format!(",{s}"))
-                  .unwrap_or_else(|| "".into())
-              ),
-              Status::Neutral,
-            ))
-          },
-          || None,
-          false,
-        );
+        let item = SectionItem::new().description(format!(
+          "{} {}: {}{}",
+          dep,
+          "[RUST]".dimmed(),
+          version_string,
+          version_suffix
+            .clone()
+            .map(|s| format!(",{s}"))
+            .unwrap_or_else(|| "".into())
+        ));
         items.push(item);
       }
     }
   }
 
-  if let Ok(rust_cli) = std::process::Command::new("cargo")
-    .arg("tauri")
-    .arg("-V")
-    .output()
-  {
-    if rust_cli.status.success() {
-      let stdout = String::from_utf8_lossy(rust_cli.stdout.as_slice()).to_string();
-      let mut output = stdout.split(' ');
-      let dep = output.next().unwrap_or_default().to_string();
-      let version_string = output
-        .next()
-        .unwrap_or_default()
-        .strip_suffix('\n')
-        .unwrap_or_default()
-        .to_string();
+  let tauri_cli_rust_item = SectionItem::new().action(|| {
+    std::process::Command::new("cargo")
+      .arg("tauri")
+      .arg("-V")
+      .output()
+      .ok()
+      .map(|o| {
+        if o.status.success() {
+          let out = String::from_utf8_lossy(o.stdout.as_slice());
+          let (package, version) = out.split_once(' ').unwrap_or_default();
+          let latest_ver = crate_latest_version(package).unwrap_or_default();
+          format!(
+            "{} {}: {}{}",
+            package,
+            "[RUST]".dimmed(),
+            version.split_once('\n').unwrap_or_default().0,
+            if !(version.is_empty() || latest_ver.is_empty()) {
+              let version = semver::Version::parse(version).unwrap();
+              let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
 
-      let version_suffix = match crate_latest_version(&dep) {
-        Some(target_version) => {
-          let version = semver::Version::parse(&version_string).unwrap();
-          let target_version = semver::Version::parse(&target_version).unwrap();
-          if version < target_version {
-            Some(format!(
-              " ({}, latest: {})",
-              "outdated".yellow(),
-              target_version.to_string().green()
-            ))
-          } else {
-            None
-          }
+              if version < target_version {
+                format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
+              } else {
+                "".into()
+              }
+            } else {
+              "".into()
+            }
+          )
+          .into()
+        } else {
+          ActionResult::None
         }
-        None => None,
-      };
-
-      items.push(SectionItem::new(
-        move || {
-          Some((
-            format!(
-              "{} {}: {}{}",
-              dep,
-              "[RUST]".dimmed(),
-              version_string,
-              version_suffix
-                .clone()
-                .map(|s| format!(", {s}"))
-                .unwrap_or_else(|| "".into())
-            ),
-            Status::Neutral,
-          ))
-        },
-        || None,
-        false,
-      ));
-    } else {
-      items.push(SectionItem::new(
-        move || {
-          Some((
-            format!("tauri-cli {}: not installed!", "[RUST]".dimmed()),
-            Status::Neutral,
-          ))
-        },
-        || None,
-        false,
-      ));
-    }
-  }
+      })
+      .unwrap_or_default()
+  });
+  items.push(tauri_cli_rust_item);
 
   items
 }